json-ld 0.1.5.2 → 0.1.6
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.
- data/History.markdown +7 -0
- data/VERSION +1 -1
- data/lib/json/ld/api.rb +86 -7
- data/lib/json/ld/compact.rb +1 -1
- data/lib/json/ld/evaluation_context.rb +11 -26
- data/lib/json/ld/expand.rb +5 -5
- data/lib/json/ld/flatten.rb +109 -0
- data/lib/json/ld/frame.rb +52 -135
- data/lib/json/ld/from_rdf.rb +16 -17
- data/lib/json/ld/utils.rb +5 -5
- data/spec/compact_spec.rb +1 -1
- data/spec/evaluation_context_spec.rb +1 -13
- data/spec/expand_spec.rb +68 -0
- data/spec/flatten_spec.rb +180 -0
- data/spec/frame_spec.rb +107 -133
- data/spec/from_rdf_spec.rb +5 -29
- metadata +5 -2
data/History.markdown
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
=== 0.1.6
|
2
|
+
* Added flattening API, and updated algorithm.
|
3
|
+
* Fixed framing issues using updated flattening.
|
4
|
+
|
5
|
+
=== 0.1.5
|
6
|
+
* Added support for @vocab.
|
7
|
+
|
1
8
|
=== 0.1.4.1
|
2
9
|
* Include rdf-xsd for some specs.
|
3
10
|
* Refactor #expand_value to deal with previous matching on RDF::Literal::Integer for sub-types.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.6
|
data/lib/json/ld/api.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'open-uri'
|
2
2
|
require 'json/ld/expand'
|
3
3
|
require 'json/ld/compact'
|
4
|
+
require 'json/ld/flatten'
|
4
5
|
require 'json/ld/frame'
|
5
6
|
require 'json/ld/to_rdf'
|
6
7
|
require 'json/ld/from_rdf'
|
@@ -19,6 +20,7 @@ module JSON::LD
|
|
19
20
|
include Expand
|
20
21
|
include Compact
|
21
22
|
include Triples
|
23
|
+
include Flatten
|
22
24
|
include FromTriples
|
23
25
|
include Frame
|
24
26
|
|
@@ -151,6 +153,77 @@ module JSON::LD
|
|
151
153
|
result
|
152
154
|
end
|
153
155
|
|
156
|
+
##
|
157
|
+
# Flattens the given input according to the steps in the Flattening Algorithm. The input must be flattened and returned if there are no errors. If the flattening fails, an appropriate exception must be thrown.
|
158
|
+
#
|
159
|
+
# The resulting `Array` is returned via the provided callback.
|
160
|
+
#
|
161
|
+
# Note that for Ruby, if the callback is not provided and a block is given, it will be yielded. If there is no block, the value will be returned.
|
162
|
+
#
|
163
|
+
# @param [String, #read, Hash, Array] input
|
164
|
+
# The JSON-LD object or array of JSON-LD objects to flatten or an IRI referencing the JSON-LD document to flatten.
|
165
|
+
# @param [String, RDF::URI] graph
|
166
|
+
# The graph in the document that should be flattened. To return the default graph @default has to be passed, for the merged graph @merged and for any other graph the IRI identifying the graph has to be passed. The default value is @merged.
|
167
|
+
# @param [String, #read, Hash, Array] context
|
168
|
+
# An optional external context to use additionally to the context embedded in input when expanding the input.
|
169
|
+
# @param [Proc] callback (&block)
|
170
|
+
# Alternative to using block, with same parameters.
|
171
|
+
# @param [Hash{Symbol => Object}] options
|
172
|
+
# Other options passed to {#expand}
|
173
|
+
# @option options [Boolean] :embed (true)
|
174
|
+
# a flag specifying that objects should be directly embedded in the output,
|
175
|
+
# instead of being referred to by their IRI.
|
176
|
+
# @option options [Boolean] :explicit (false)
|
177
|
+
# a flag specifying that for properties to be included in the output,
|
178
|
+
# they must be explicitly declared in the framing context.
|
179
|
+
# @option options [Boolean] :omitDefault (false)
|
180
|
+
# a flag specifying that properties that are missing from the JSON-LD
|
181
|
+
# input should be omitted from the output.
|
182
|
+
# @yield jsonld
|
183
|
+
# @yieldparam [Hash] jsonld
|
184
|
+
# The framed JSON-LD document
|
185
|
+
# @return [Array<Hash>]
|
186
|
+
# The framed JSON-LD document
|
187
|
+
# @raise [InvalidFrame]
|
188
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#framing-algorithm
|
189
|
+
def self.flatten(input, graph, context, callback = nil, options = {})
|
190
|
+
result = nil
|
191
|
+
graph ||= '@merged'
|
192
|
+
|
193
|
+
# Expand input to simplify processing
|
194
|
+
expanded_input = API.expand(input)
|
195
|
+
|
196
|
+
# Initialize input using frame as context
|
197
|
+
API.new(expanded_input, nil, options) do
|
198
|
+
debug(".flatten") {"expanded input: #{value.to_json(JSON_STATE)}"}
|
199
|
+
|
200
|
+
# Generate _nodeMap_
|
201
|
+
node_map = Hash.ordered
|
202
|
+
self.generate_node_map(value,
|
203
|
+
node_map,
|
204
|
+
(graph.to_s == '@merged' ? '@merged' : '@default'),
|
205
|
+
nil,
|
206
|
+
BlankNodeNamer.new("t"))
|
207
|
+
|
208
|
+
result = []
|
209
|
+
|
210
|
+
# If nodeMap has no property graph, return result, otherwise set definitions to its value.
|
211
|
+
definitions = node_map.fetch(graph.to_s, {})
|
212
|
+
|
213
|
+
# Foreach property and valud of definitions
|
214
|
+
definitions.keys.sort.each do |prop|
|
215
|
+
value = definitions[prop]
|
216
|
+
result << value
|
217
|
+
end
|
218
|
+
|
219
|
+
result
|
220
|
+
end
|
221
|
+
|
222
|
+
callback.call(result) if callback
|
223
|
+
yield result if block_given?
|
224
|
+
result
|
225
|
+
end
|
226
|
+
|
154
227
|
##
|
155
228
|
# Frames the given input using the frame according to the steps in the Framing Algorithm. The input is used to build the
|
156
229
|
# framed output and is returned if there are no errors. If there are no matches for the frame, null must be returned.
|
@@ -219,13 +292,20 @@ module JSON::LD
|
|
219
292
|
#debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE)}"}
|
220
293
|
#debug(".frame") {"expanded input: #{value.to_json(JSON_STATE)}"}
|
221
294
|
|
222
|
-
# Get framing
|
223
|
-
|
224
|
-
depth
|
225
|
-
|
295
|
+
# Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
|
296
|
+
all_nodes = Hash.ordered
|
297
|
+
depth do
|
298
|
+
generate_node_map(value,
|
299
|
+
all_nodes,
|
300
|
+
'@merged',
|
301
|
+
nil,
|
302
|
+
BlankNodeNamer.new("t"))
|
303
|
+
end
|
304
|
+
@node_map = all_nodes['@merged']
|
305
|
+
debug(".frame") {"node_map: #{@node_map.to_json(JSON_STATE)}"}
|
226
306
|
|
227
307
|
result = []
|
228
|
-
frame(framing_state, @
|
308
|
+
frame(framing_state, @node_map, expanded_frame[0], result, nil)
|
229
309
|
debug(".frame") {"after frame: #{result.to_json(JSON_STATE)}"}
|
230
310
|
|
231
311
|
# Initalize context from frame
|
@@ -288,8 +368,7 @@ module JSON::LD
|
|
288
368
|
# @param [Proc] callback (&block)
|
289
369
|
# Alternative to using block, with same parameteres.
|
290
370
|
# @param [Hash{Symbol => Object}] options
|
291
|
-
# @option options [Boolean] :
|
292
|
-
# @option options [Boolean] :useNativeDatatypes FIXME
|
371
|
+
# @option options [Boolean] :notType don't use @type for rdf:type
|
293
372
|
# @yield jsonld
|
294
373
|
# @yieldparam [Hash] jsonld
|
295
374
|
# The JSON-LD document in expanded form
|
data/lib/json/ld/compact.rb
CHANGED
@@ -44,7 +44,7 @@ module JSON::LD
|
|
44
44
|
|
45
45
|
case k
|
46
46
|
when '@value', '@id'
|
47
|
-
# If element has an @value property or element is a
|
47
|
+
# If element has an @value property or element is a node reference, return the result of performing
|
48
48
|
# Value Compaction on element using active property.
|
49
49
|
v = context.compact_value(property, element, :depth => @depth)
|
50
50
|
debug("compact") {"value optimization, return as #{v.inspect}"}
|
@@ -13,11 +13,6 @@ module JSON::LD
|
|
13
13
|
# @attr_reader [RDF::URI]
|
14
14
|
attr_reader :base
|
15
15
|
|
16
|
-
# The base IRI of the context, if loaded remotely.
|
17
|
-
#
|
18
|
-
# @attr [RDF::URI]
|
19
|
-
attr :context_base, true
|
20
|
-
|
21
16
|
# A list of current, in-scope mappings from term to IRI.
|
22
17
|
#
|
23
18
|
# @attr [Hash{String => String}]
|
@@ -137,26 +132,18 @@ module JSON::LD
|
|
137
132
|
raise JSON::LD::InvalidContext::Syntax, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
|
138
133
|
self.dup
|
139
134
|
end
|
140
|
-
when nil
|
141
|
-
debug("parse") {"nil"}
|
142
|
-
# Load context document, if it is a string
|
143
|
-
ec = EvaluationContext.new(options)
|
144
|
-
when String
|
135
|
+
when String, nil
|
145
136
|
debug("parse") {"remote: #{context}"}
|
146
137
|
# Load context document, if it is a string
|
147
138
|
ec = nil
|
148
139
|
begin
|
149
|
-
|
150
|
-
ecdup = self.dup
|
151
|
-
ecdup.context_base = url # Set context_base for recursive remote contexts
|
152
|
-
RDF::Util::File.open_file(url) {|f| ec = ecdup.parse(f)}
|
140
|
+
RDF::Util::File.open_file(context.to_s) {|f| ec = parse(f)}
|
153
141
|
ec.provided_context = context
|
154
|
-
ec.context_base = url
|
155
142
|
debug("parse") {"=> provided_context: #{context.inspect}"}
|
156
143
|
ec
|
157
144
|
rescue Exception => e
|
158
|
-
debug("parse") {"Failed to retrieve @context from remote document at #{context
|
159
|
-
raise JSON::LD::InvalidContext::LoadError, "Failed to
|
145
|
+
debug("parse") {"Failed to retrieve @context from remote document at #{context}: #{e.message}"}
|
146
|
+
raise JSON::LD::InvalidContext::LoadError, "Failed to parse remote context at #{context}: #{e.message}", e.backtrace if @options[:validate]
|
160
147
|
self.dup
|
161
148
|
end
|
162
149
|
when Array
|
@@ -480,8 +467,6 @@ module JSON::LD
|
|
480
467
|
# @param [Hash{Symbol => Object}] options
|
481
468
|
# @option options [:subject, :predicate, :object, :datatype] position
|
482
469
|
# Useful when determining how to serialize.
|
483
|
-
# @option options [RDF::URI] base (self.base)
|
484
|
-
# Base IRI to use when expanding relative IRIs.
|
485
470
|
#
|
486
471
|
# @return [RDF::URI, String] IRI or String, if it's a keyword
|
487
472
|
# @raise [RDF::ReaderError] if the iri cannot be expanded
|
@@ -491,7 +476,7 @@ module JSON::LD
|
|
491
476
|
prefix, suffix = iri.split(':', 2)
|
492
477
|
return mapping(iri) if mapping(iri) # If it's an exact match
|
493
478
|
debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{vocab.inspect}"} unless options[:quiet]
|
494
|
-
base =
|
479
|
+
base = [:subject, :object].include?(options[:position]) ? self.base : nil
|
495
480
|
prefix = prefix.to_s
|
496
481
|
case
|
497
482
|
when prefix == '_' && suffix then bnode(suffix)
|
@@ -669,13 +654,13 @@ module JSON::LD
|
|
669
654
|
# @param [Hash, String] value
|
670
655
|
# Value (literal or IRI) to be expanded
|
671
656
|
# @param [Hash{Symbol => Object}] options
|
672
|
-
# @option options [Boolean] :
|
657
|
+
# @option options [Boolean] :native (true) use native representations
|
673
658
|
#
|
674
659
|
# @return [Hash] Object representation of value
|
675
660
|
# @raise [RDF::ReaderError] if the iri cannot be expanded
|
676
661
|
# @see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
|
677
662
|
def expand_value(property, value, options = {})
|
678
|
-
options = {:
|
663
|
+
options = {:native => true}.merge(options)
|
679
664
|
depth(options) do
|
680
665
|
debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
|
681
666
|
value = RDF::Literal(value) if RDF::Literal(value).has_datatype?
|
@@ -696,7 +681,7 @@ module JSON::LD
|
|
696
681
|
when RDF::XSD.double.to_s
|
697
682
|
{"@value" => value.to_s, "@type" => RDF::XSD.double.to_s}
|
698
683
|
else
|
699
|
-
if options[:
|
684
|
+
if options[:native]
|
700
685
|
# Unless there's coercion, to not modify representation
|
701
686
|
{"@value" => (value.is_a?(RDF::Literal::Boolean) ? value.object : value)}
|
702
687
|
else
|
@@ -710,7 +695,7 @@ module JSON::LD
|
|
710
695
|
{"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
|
711
696
|
when RDF::XSD.integer.to_s, nil
|
712
697
|
# Unless there's coercion, to not modify representation
|
713
|
-
if options[:
|
698
|
+
if options[:native]
|
714
699
|
{"@value" => value.is_a?(RDF::Literal::Integer) ? value.object : value}
|
715
700
|
else
|
716
701
|
{"@value" => value.to_s, "@type" => RDF::XSD.integer.to_s}
|
@@ -729,7 +714,7 @@ module JSON::LD
|
|
729
714
|
when RDF::XSD.double.to_s
|
730
715
|
{"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
|
731
716
|
when nil
|
732
|
-
if options[:
|
717
|
+
if options[:native]
|
733
718
|
# Unless there's coercion, to not modify representation
|
734
719
|
{"@value" => value.is_a?(RDF::Literal::Double) ? value.object : value}
|
735
720
|
else
|
@@ -940,7 +925,7 @@ module JSON::LD
|
|
940
925
|
else
|
941
926
|
val_lang == language(term) ? 3 : (default_term ? 1 : 0)
|
942
927
|
end
|
943
|
-
else #
|
928
|
+
else # node definition/reference
|
944
929
|
coerce(term) == '@id' ? 3 : (default_term ? 1 : 0)
|
945
930
|
end
|
946
931
|
|
data/lib/json/ld/expand.rb
CHANGED
@@ -76,17 +76,17 @@ module JSON::LD
|
|
76
76
|
when Array
|
77
77
|
depth do
|
78
78
|
[value].flatten.map do |v|
|
79
|
-
v = v['@id'] if
|
80
|
-
raise ProcessingError, "Object value must be a string or a
|
79
|
+
v = v['@id'] if node_reference?(v)
|
80
|
+
raise ProcessingError, "Object value must be a string or a node reference: #{v.inspect}" unless v.is_a?(String)
|
81
81
|
context.expand_iri(v, options.merge(:position => :property, :quiet => true)).to_s
|
82
82
|
end
|
83
83
|
end
|
84
84
|
when Hash
|
85
|
-
# Empty object used for @type wildcard or
|
86
|
-
if
|
85
|
+
# Empty object used for @type wildcard or node reference
|
86
|
+
if node_reference?(value)
|
87
87
|
context.expand_iri(value['@id'], options.merge(:position => :property, :quiet => true)).to_s
|
88
88
|
elsif !value.empty?
|
89
|
-
raise ProcessingError, "Object value of @type must be empty or a
|
89
|
+
raise ProcessingError, "Object value of @type must be empty or a node reference: #{value.inspect}"
|
90
90
|
else
|
91
91
|
value
|
92
92
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module JSON::LD
|
2
|
+
module Flatten
|
3
|
+
include Utils
|
4
|
+
|
5
|
+
##
|
6
|
+
# Build hash of nodes used for framing. Also returns flattened representation of input.
|
7
|
+
#
|
8
|
+
# @param [Array, Hash] element
|
9
|
+
# Expanded element
|
10
|
+
# @param [Hash{String => Hash}] node_map
|
11
|
+
# map of nodes
|
12
|
+
# @param [String] graph
|
13
|
+
# Graph name for results
|
14
|
+
# @param [Array] list
|
15
|
+
# List for saving list elements
|
16
|
+
# @param [BlankNodeNamer] namer
|
17
|
+
# @param [String] id
|
18
|
+
# Identifier already associated with element
|
19
|
+
def generate_node_map(element, node_map, graph, list, namer, id = nil)
|
20
|
+
depth do
|
21
|
+
debug("nodeMap") {"element: #{element.inspect}, graph: #{graph}"}
|
22
|
+
if element.is_a?(Array)
|
23
|
+
element.map {|o| generate_node_map(o, node_map, graph, list, namer)}
|
24
|
+
elsif !element.is_a?(Hash) || value?(element)
|
25
|
+
list << element if list
|
26
|
+
else
|
27
|
+
# If the @id property exists and is an IRI, set id to its value, otherwise set it to a blank node identifier created by the Generate Blank Node Identifier algorithm.
|
28
|
+
# FIXME: (spec) id passed when it is allocated, so it is not re-allocated here.
|
29
|
+
id ||= blank_node?(element) ? namer.get_name(element.fetch('@id', nil)) : element['@id']
|
30
|
+
|
31
|
+
# If list is not nil, append a new node reference to list using id at the value for @id.
|
32
|
+
list << {'@id' => id} if list
|
33
|
+
|
34
|
+
# Let nodes be the value in nodeMap where the key is graph; if no such value exists, insert a new JSON object for the key graph.
|
35
|
+
debug("nodeMap") {"new graph: #{graph}"} unless node_map.has_key?(graph)
|
36
|
+
nodes = (node_map[graph] ||= Hash.ordered)
|
37
|
+
|
38
|
+
# If id is not in nodes, create a new JSON object node with id as the value for @id. Let node be the value of id in nodes.
|
39
|
+
debug("nodeMap") {"new node: #{id}"} unless nodes.has_key?(id)
|
40
|
+
node = (nodes[id] ||= Hash.ordered)
|
41
|
+
node['@id'] ||= id
|
42
|
+
|
43
|
+
# For each property that is not @id and each value in element ordered by property:
|
44
|
+
element.each do |prop, value|
|
45
|
+
case prop
|
46
|
+
when '@id'
|
47
|
+
# Skip @id, already assigned
|
48
|
+
when '@graph'
|
49
|
+
# If property is @graph, recursively call this algorithm passing value for element, nodeMap, nil for list and if graph is @merged use graph, otherwise use id for graph and then continue.
|
50
|
+
graph = graph == '@merged' ? '@merged' : id
|
51
|
+
generate_node_map(value, node_map, graph, nil, namer)
|
52
|
+
when /^@(?!type)/
|
53
|
+
# If property is not @type and is a keyword, merge property and value into node and then continue.
|
54
|
+
debug("nodeMap") {"merge keyword#{prop}: #{value.inspect}"}
|
55
|
+
node[prop] = value
|
56
|
+
else
|
57
|
+
raise InvalidFrame::Syntax,
|
58
|
+
"unexpected value: #{value.inspect}, expected array" unless
|
59
|
+
value.is_a?(Array)
|
60
|
+
|
61
|
+
# For each value v in the array value:
|
62
|
+
value.each do |v|
|
63
|
+
if node?(v) || node_reference?(v)
|
64
|
+
debug("nodeMap") {"node value #{prop}: #{v.inspect}"}
|
65
|
+
# If v is a node definition or node reference:
|
66
|
+
# If the property @id is not an IRI or it does not exist, map v to a new blank node identifier to avoid collisions.
|
67
|
+
name = blank_node?(v) ?
|
68
|
+
namer.get_name(v.fetch('@id', nil)) :
|
69
|
+
v['@id']
|
70
|
+
|
71
|
+
# If one does not already exist, add a node reference for v into node for property.
|
72
|
+
node[prop] ||= []
|
73
|
+
node[prop] << {'@id' => name} unless node[prop].any? {|n|
|
74
|
+
node_reference?(n) && n['@id'] == name
|
75
|
+
}
|
76
|
+
|
77
|
+
# Recursively call this algorithm passing v for value, nodeMap, graph, and nil for list.
|
78
|
+
generate_node_map(v, node_map, graph, nil, namer, name)
|
79
|
+
elsif list?(v)
|
80
|
+
# Otherwise if v has the property @list then recursively call this algorithm with the value of @list as element, nodeMap, graph, and a new array flattenedList as list.
|
81
|
+
debug("nodeMap") {"list value #{prop}: #{v.inspect}"}
|
82
|
+
flattened_list = []
|
83
|
+
generate_node_map(v['@list'],
|
84
|
+
node_map,
|
85
|
+
graph,
|
86
|
+
flattened_list,
|
87
|
+
namer)
|
88
|
+
# Create a new JSON object with the property @list set to flattenedList and add it to node for property.
|
89
|
+
(node[prop] ||= []) << {'@list' => flattened_list}
|
90
|
+
elsif prop == '@type'
|
91
|
+
# Otherwise, if property is @type and v is not an IRI, generate a new blank node identifier and add it to node for property.
|
92
|
+
# FIXME: @type shouldn't really be a BNode, and not clear how we even get here from expansion
|
93
|
+
name = blank_node?({'@id' => v}) ? namer.get_name(v) : v
|
94
|
+
(node[prop] ||= []) << name
|
95
|
+
else
|
96
|
+
debug("nodeMap") {"value #{prop}: #{v.inspect}"}
|
97
|
+
# Otherwise, add v to node for property.
|
98
|
+
(node[prop] ||= []) << v
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
debug("nodeMap") {node_map.to_json(JSON_STATE)}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/json/ld/frame.rb
CHANGED
@@ -7,28 +7,27 @@ module JSON::LD
|
|
7
7
|
#
|
8
8
|
# @param [Hash{Symbol => Object}] state
|
9
9
|
# Current framing state
|
10
|
-
# @param [Hash{String => Hash}]
|
11
|
-
# Map of flattened
|
10
|
+
# @param [Hash{String => Hash}] nodes
|
11
|
+
# Map of flattened nodes
|
12
12
|
# @param [Hash{String => Object}] frame
|
13
13
|
# @param [Hash{String => Object}] parent
|
14
|
-
# Parent
|
14
|
+
# Parent node or top-level array
|
15
15
|
# @param [String] property
|
16
16
|
# Property referencing this frame, or null for array.
|
17
17
|
# @raise [JSON::LD::InvalidFrame]
|
18
|
-
def frame(state,
|
19
|
-
raise ProcessingError, "why isn't @subjects a hash?: #{@subjects.inspect}" unless @subjects.is_a?(Hash)
|
18
|
+
def frame(state, nodes, frame, parent, property)
|
20
19
|
depth do
|
21
20
|
debug("frame") {"state: #{state.inspect}"}
|
22
|
-
debug("frame") {"
|
21
|
+
debug("frame") {"nodes: #{nodes.keys.inspect}"}
|
23
22
|
debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
|
24
23
|
debug("frame") {"parent: #{parent.to_json(JSON_STATE)}"}
|
25
24
|
debug("frame") {"property: #{property.inspect}"}
|
26
25
|
# Validate the frame
|
27
26
|
validate_frame(state, frame)
|
28
27
|
|
29
|
-
# Create a set of matched
|
28
|
+
# Create a set of matched nodes by filtering nodes by checking the map of flattened nodes against frame
|
30
29
|
# This gives us a hash of objects indexed by @id
|
31
|
-
matches =
|
30
|
+
matches = filter_nodes(state, nodes, frame)
|
32
31
|
debug("frame") {"matches: #{matches.keys.inspect}"}
|
33
32
|
|
34
33
|
# Get values for embedOn and explicitOn
|
@@ -36,7 +35,7 @@ module JSON::LD
|
|
36
35
|
explicit = get_frame_flag(state, frame, 'explicit');
|
37
36
|
debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
|
38
37
|
|
39
|
-
# For each id and
|
38
|
+
# For each id and node from the set of matched nodes ordered by id
|
40
39
|
matches.keys.sort.each do |id|
|
41
40
|
element = matches[id]
|
42
41
|
# If the active property is null, set the map of embeds in state to an empty map
|
@@ -45,7 +44,7 @@ module JSON::LD
|
|
45
44
|
output = {'@id' => id}
|
46
45
|
|
47
46
|
# prepare embed meta info
|
48
|
-
|
47
|
+
embedded_node = {:parent => parent, :property => property}
|
49
48
|
|
50
49
|
# If embedOn is true, and id is in map of embeds from state
|
51
50
|
if embed && (existing = state[:embeds].fetch(id, nil))
|
@@ -58,7 +57,7 @@ module JSON::LD
|
|
58
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
|
59
58
|
existing[:parent].detect {|p| p['@id'] == id}
|
60
59
|
else
|
61
|
-
# Otherwise, existing has a parent which is a
|
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
|
62
61
|
existing[:parent].fetch(existing[:property], []).any? do |v|
|
63
62
|
v.is_a?(Hash) && v.fetch('@id', nil) == id
|
64
63
|
end
|
@@ -74,14 +73,14 @@ module JSON::LD
|
|
74
73
|
add_frame_output(state, parent, property, output)
|
75
74
|
else
|
76
75
|
# Add embed to map of embeds for id
|
77
|
-
state[:embeds][id] =
|
78
|
-
debug("frame") {"add
|
76
|
+
state[:embeds][id] = embedded_node
|
77
|
+
debug("frame") {"add embedded_node: #{embedded_node.inspect}"}
|
79
78
|
|
80
|
-
# Process each property and value in the matched
|
79
|
+
# Process each property and value in the matched node as follows
|
81
80
|
element.keys.sort.each do |prop|
|
82
81
|
value = element[prop]
|
83
82
|
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
|
83
|
+
# If property is a keyword, add property and a copy of value to output and continue with the next property from node
|
85
84
|
output[prop] = value.dup
|
86
85
|
next
|
87
86
|
end
|
@@ -89,7 +88,7 @@ module JSON::LD
|
|
89
88
|
# If property is not in frame:
|
90
89
|
unless frame.has_key?(prop)
|
91
90
|
debug("frame") {"non-framed property #{prop}"}
|
92
|
-
# If explicitOn is false, Embed values from
|
91
|
+
# If explicitOn is false, Embed values from node in output using node as element and property as active property
|
93
92
|
embed_values(state, element, prop, output) unless explicit
|
94
93
|
|
95
94
|
# Continue to next property
|
@@ -110,29 +109,29 @@ module JSON::LD
|
|
110
109
|
|
111
110
|
# Process each listitem in the @list array as follows
|
112
111
|
item['@list'].each do |listitem|
|
113
|
-
if
|
112
|
+
if node_reference?(listitem)
|
114
113
|
itemid = listitem['@id']
|
115
114
|
debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}
|
116
115
|
|
117
|
-
# If listitem is a
|
118
|
-
frame(state, {itemid => @
|
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, list, '@list')
|
119
118
|
else
|
120
119
|
# Otherwise, append a copy of listitem to @list in list.
|
121
|
-
debug("frame") {"list item of #{prop} non-
|
120
|
+
debug("frame") {"list item of #{prop} non-node ref #{listitem.inspect}"}
|
122
121
|
add_frame_output(state, list, '@list', listitem)
|
123
122
|
end
|
124
123
|
end
|
125
|
-
elsif
|
126
|
-
# If item is a
|
124
|
+
elsif node_reference?(item)
|
125
|
+
# If item is a node reference process item recursively
|
127
126
|
# Recurse into sub-objects
|
128
127
|
itemid = item['@id']
|
129
128
|
debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
|
130
129
|
|
131
|
-
# passing a new map as
|
132
|
-
frame(state, {itemid => @
|
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, output, prop)
|
133
132
|
else
|
134
133
|
# Otherwise, append a copy of item to active property in output.
|
135
|
-
debug("frame") {"value property #{prop} non-
|
134
|
+
debug("frame") {"value property #{prop} non-node ref #{item.inspect}"}
|
136
135
|
add_frame_output(state, output, prop, item)
|
137
136
|
end
|
138
137
|
end
|
@@ -164,89 +163,6 @@ module JSON::LD
|
|
164
163
|
end
|
165
164
|
end
|
166
165
|
|
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)}
|
248
|
-
end
|
249
|
-
|
250
166
|
##
|
251
167
|
# Replace @preserve keys with the values, also replace @null with null
|
252
168
|
#
|
@@ -289,50 +205,51 @@ module JSON::LD
|
|
289
205
|
private
|
290
206
|
|
291
207
|
##
|
292
|
-
# Returns a map of all of the
|
208
|
+
# Returns a map of all of the nodes that match a parsed frame.
|
293
209
|
#
|
294
210
|
# @param state the current framing state.
|
295
|
-
# @param
|
211
|
+
# @param nodes the set of nodes to filter.
|
296
212
|
# @param frame the parsed frame.
|
297
213
|
#
|
298
|
-
# @return all of the matched
|
299
|
-
def
|
300
|
-
|
214
|
+
# @return all of the matched nodes.
|
215
|
+
def filter_nodes(state, nodes, frame)
|
216
|
+
nodes.dup.keep_if {|id, element| element && filter_node(state, element, frame)}
|
301
217
|
end
|
302
218
|
|
303
219
|
##
|
304
|
-
# Returns true if the given
|
220
|
+
# Returns true if the given node matches the given frame.
|
305
221
|
#
|
306
|
-
# Matches either based on explicit type inclusion where the
|
222
|
+
# Matches either based on explicit type inclusion where the node
|
307
223
|
# has any type listed in the frame. If the frame has empty types defined
|
308
|
-
# matches
|
309
|
-
# matches
|
224
|
+
# matches nodes not having a @type. If the frame has a type of {} defined
|
225
|
+
# matches nodes having any type defined.
|
310
226
|
#
|
311
|
-
# Otherwise, does duck typing, where the
|
227
|
+
# Otherwise, does duck typing, where the node must have all of the properties
|
312
228
|
# defined in the frame.
|
313
229
|
#
|
314
230
|
# @param [Hash{Symbol => Object}] state the current frame state.
|
315
|
-
# @param [Hash{String => Object}]
|
231
|
+
# @param [Hash{String => Object}] node the node to check.
|
316
232
|
# @param [Hash{String => Object}] frame the frame to check.
|
317
233
|
#
|
318
|
-
# @return true if the
|
319
|
-
def
|
234
|
+
# @return true if the node matches, false if not.
|
235
|
+
def filter_node(state, node, frame)
|
236
|
+
debug("frame") {"filter node: #{node.inspect}"}
|
320
237
|
if types = frame.fetch('@type', nil)
|
321
|
-
|
238
|
+
node_types = node.fetch('@type', [])
|
322
239
|
raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
|
323
|
-
raise InvalidFrame::Syntax, "
|
324
|
-
# If frame has an @type, use it for selecting appropriate
|
325
|
-
debug("frame") {"filter
|
240
|
+
raise InvalidFrame::Syntax, "node @type must be an array: #{node_types.inspect}" unless node_types.is_a?(Array)
|
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}"}
|
326
243
|
|
327
244
|
# Check for type wild-card, or intersection
|
328
|
-
types == [{}] ? !
|
245
|
+
types == [{}] ? !node_types.empty? : node_types.any? {|t| types.include?(t)}
|
329
246
|
else
|
330
|
-
# Duck typing, for
|
247
|
+
# Duck typing, for nodes not having a type, but having @id
|
331
248
|
|
332
249
|
# Subject matches if it has all the properties in the frame
|
333
250
|
frame_keys = frame.keys.reject {|k| k[0,1] == '@'}
|
334
|
-
|
335
|
-
(frame_keys &
|
251
|
+
node_keys = node.keys.reject {|k| k[0,1] == '@'}
|
252
|
+
(frame_keys & node_keys) == frame_keys
|
336
253
|
end
|
337
254
|
end
|
338
255
|
|
@@ -361,13 +278,13 @@ module JSON::LD
|
|
361
278
|
property = embed[:property];
|
362
279
|
|
363
280
|
# create reference to replace embed
|
364
|
-
|
365
|
-
|
281
|
+
node = {}
|
282
|
+
node['@id'] = id
|
366
283
|
ref = {'@id' => id}
|
367
284
|
|
368
285
|
# remove existing embed
|
369
|
-
if
|
370
|
-
# replace
|
286
|
+
if node?(parent)
|
287
|
+
# replace node with reference
|
371
288
|
parent[property].map! do |v|
|
372
289
|
v.is_a?(Hash) && v.fetch('@id', nil) == id ? ref : v
|
373
290
|
end
|
@@ -418,7 +335,7 @@ module JSON::LD
|
|
418
335
|
def embed_values(state, element, property, output)
|
419
336
|
element[property].each do |o|
|
420
337
|
# Get element @id, if this is an object
|
421
|
-
sid = o['@id'] if
|
338
|
+
sid = o['@id'] if node_reference?(o)
|
422
339
|
if sid
|
423
340
|
unless state[:embeds].has_key?(sid)
|
424
341
|
debug("frame") {"embed element #{sid.inspect}"}
|
@@ -427,7 +344,7 @@ module JSON::LD
|
|
427
344
|
state[:embeds][sid] = embed
|
428
345
|
|
429
346
|
# Recurse into element
|
430
|
-
s = @
|
347
|
+
s = @node_map.fetch(sid, {'@id' => sid})
|
431
348
|
o = {}
|
432
349
|
s.each do |prop, value|
|
433
350
|
if prop[0,1] == '@'
|