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.
@@ -0,0 +1,97 @@
1
+ module JSON::LD
2
+ module Utils
3
+ ##
4
+ # Is value a subject? A value is a subject if
5
+ # * it is a Hash
6
+ # * it is not a @value, @set or @list
7
+ # * it has more than 1 key or any key is not @id
8
+ # @param [Object] value
9
+ # @return [Boolean]
10
+ def subject?(value)
11
+ value.is_a?(Hash) &&
12
+ (value.keys & %w(@value @list @set)).empty? &&
13
+ !(value.keys - ['@id']).empty?
14
+ end
15
+
16
+ ##
17
+ # Is value a subject reference?
18
+ # @param [Object] value
19
+ # @return [Boolean]
20
+ def subject_reference?(value)
21
+ value.is_a?(Hash) && value.keys == %w(@id)
22
+ end
23
+
24
+ ##
25
+ # Is value a blank node? Value is a blank node
26
+ #
27
+ # @param [Object] value
28
+ # @return [Boolean]
29
+ def blank_node?(value)
30
+ (subject?(value) || subject_reference?(value)) && value.fetch('@id', '_:')[0,2] == '_:'
31
+ end
32
+
33
+ ##
34
+ # Is value an expaned @list?
35
+ #
36
+ # @param [Object] value
37
+ # @return [Boolean]
38
+ def list?(value)
39
+ value.is_a?(Hash) && value.keys == %w(@list)
40
+ end
41
+
42
+ private
43
+
44
+ # Add debug event to debug array, if specified
45
+ #
46
+ # @param [String] message
47
+ # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
48
+ def debug(*args)
49
+ return unless ::JSON::LD.debug? || @options[:debug]
50
+ list = args
51
+ list << yield if block_given?
52
+ message = " " * (@depth || 0) * 2 + (list.empty? ? "" : list.join(": "))
53
+ puts message if JSON::LD::debug?
54
+ @options[:debug] << message if @options[:debug].is_a?(Array)
55
+ end
56
+
57
+ # Increase depth around a method invocation
58
+ def depth(options = {})
59
+ old_depth = @depth || 0
60
+ @depth = (options[:depth] || old_depth) + 1
61
+ ret = yield
62
+ @depth = old_depth
63
+ ret
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Utility class for mapping old blank node identifiers, or unnamed blank
69
+ # nodes to new identifiers
70
+ class BlankNodeNamer < Hash
71
+ # @param [String] prefix
72
+ def initialize(prefix)
73
+ @prefix = "_:#{prefix}0"
74
+ super
75
+ end
76
+
77
+ ##
78
+ # Get a new mapped name for `old`
79
+ #
80
+ # @param [String] old
81
+ # @return [String]
82
+ def get_name(old)
83
+ if old && self.has_key?(old)
84
+ self[old]
85
+ elsif old
86
+ self[old] = @prefix.dup
87
+ @prefix.succ!
88
+ self[old]
89
+ else
90
+ # Not referenced, just return a new unique value
91
+ cur = @prefix.dup
92
+ @prefix.succ!
93
+ cur
94
+ end
95
+ end
96
+ end
97
+ end
@@ -51,6 +51,7 @@ module JSON::LD
51
51
  # @see http://json-ld.org/spec/ED/20110507/#the-normalization-algorithm
52
52
  # @author [Gregg Kellogg](http://greggkellogg.net/)
53
53
  class Writer < RDF::Writer
54
+ include Utils
54
55
  format Format
55
56
 
56
57
  # @attr [RDF::Graph] Graph of statements serialized
@@ -60,13 +61,9 @@ module JSON::LD
60
61
  attr :context
61
62
 
62
63
  ##
63
- # Return the pre-serialized Hash before turning into JSON
64
- #
65
- # @return [Hash]
66
- def self.hash(*args, &block)
67
- hash = Hash.new
68
- self.new(hash, *args, &block)
69
- hash
64
+ # Override normal symbol generation
65
+ def self.to_sym
66
+ :jsonld
70
67
  end
71
68
 
72
69
  ##
@@ -80,33 +77,22 @@ module JSON::LD
80
77
  # the encoding to use on the output stream (Ruby 1.9+)
81
78
  # @option options [Boolean] :canonicalize (false)
82
79
  # whether to canonicalize literals when serializing
83
- # @option options [Hash] :prefixes (Hash.new)
80
+ # @option options [Hash] :prefixes (Hash.ordered)
84
81
  # the prefix mappings to use (not supported by all writers)
85
82
  # @option options [Boolean] :standard_prefixes (false)
86
83
  # Add standard prefixes to @prefixes, if necessary.
87
- # @option options [IO, Array, Hash, String, EvaluationContext] :context (Hash.new)
84
+ # @option options [IO, Array, Hash, String, EvaluationContext] :context (Hash.ordered)
88
85
  # context to use when serializing. Constructed context for native serialization.
89
- # @option options [Boolean] :automatic (true)
90
- # Automatically create context coercions and generate compacted form
91
- # @option options [Boolean] :expand (false)
92
- # Output document in [expanded form](http://json-ld.org/spec/latest/json-ld-api/#expansion)
93
- # @option options [Boolean] :compact (false)
94
- # Output document in [compacted form](http://json-ld.org/spec/latest/json-ld-api/#compaction).
95
- # Requires a referenced evaluation context
96
- # @option options [Boolean] :normalize (false)
97
- # Output document in [normalized form](http://json-ld.org/spec/latest/json-ld-api/#normalization)
98
- # @option options [IO, Array, Hash, String] :frame
99
- # Output document in [framed form](http://json-ld.org/spec/latest/json-ld-api/#framing)
100
- # using the referenced document as a frame.
101
86
  # @yield [writer] `self`
102
87
  # @yieldparam [RDF::Writer] writer
103
88
  # @yieldreturn [void]
104
89
  # @yield [writer]
105
90
  # @yieldparam [RDF::Writer] writer
106
91
  def initialize(output = $stdout, options = {}, &block)
92
+ options[:base_uri] ||= options[:base] if options.has_key?(:base)
93
+ options[:base] ||= options[:base_uri] if options.has_key?(:base_uri)
107
94
  super do
108
95
  @graph = RDF::Graph.new
109
- @options[:automatic] = true unless [:automatic, :expand, :compact, :frame, :normalize].any? {|k| options.has_key?(k)}
110
96
 
111
97
  if block_given?
112
98
  case block.arity
@@ -128,7 +114,7 @@ module JSON::LD
128
114
  end
129
115
 
130
116
  ##
131
- # Addes a statement to be serialized
117
+ # Adds a statement to be serialized
132
118
  # @param [RDF::Statement] statement
133
119
  # @return [void]
134
120
  def write_statement(statement)
@@ -148,464 +134,40 @@ module JSON::LD
148
134
  end
149
135
 
150
136
  ##
151
- # Outputs the Serialized JSON-LD representation of all stored triples.
137
+ # Outputs the Serialized JSON-LD representation of all stored statements.
138
+ #
139
+ # If provided a context or prefixes, we'll create a context
140
+ # and use it to compact the output. Otherwise, we return un-compacted JSON-LD
152
141
  #
153
142
  # @return [void]
154
143
  # @see #write_triple
155
144
  def write_epilogue
156
145
  @debug = @options[:debug]
157
146
 
158
- reset
159
-
160
- raise RDF::WriterError, "Compaction requres a context" if @options[:compact] && !@options[:context]
161
-
162
- @context = EvaluationContext.new(@options)
163
- @context = @context.parse(@options[:context]) if @options[:context]
164
- @context.language = @options[:language] if @options[:language]
165
- @context.lists.each {|p| @list_range[p] = true}
166
-
167
- debug {"\nserialize: graph: #{@graph.size}"}
168
- debug {"=> options: #{@options.reject {|k,v| k == :debug}.inspect}"}
169
- debug {"=> context: #{@context.inspect}"}
170
-
171
- preprocess
172
-
173
- # Update prefix mappings to those defined in context
174
- @options[:prefixes] = {}
175
- @context.iri_to_term.each_pair do |iri, term|
176
- debug {"add prefix #{term.inspect} for #{iri}"}
177
- prefix(term, iri) # Define for output
178
- end
179
-
180
- # Don't generate context for expanded or normalized output
181
- json_hash = (@options[:expand] || @options[:normalize]) ? Hash.new : context.serialize(:depth => @depth)
182
-
183
- elements = []
184
- order_subjects.each do |subject|
185
- unless is_done?(subject)
186
- elements << subject(subject, json_hash)
187
- end
188
- end
189
-
190
- return if elements.empty?
191
-
192
- # If there are more than one top-level subjects, place in an array form
193
- if elements.length == 1 && elements.first.is_a?(Hash)
194
- json_hash.merge!(elements.first)
195
- else
196
- json_hash['@id'] = elements
197
- end
198
-
199
- if @output.is_a?(Hash)
200
- @output.merge!(json_hash)
201
- else
202
- json_state = if @options[:normalize]
203
- # Normalization uses a compressed form
204
- JSON::State.new(
205
- :indent => "",
206
- :space => "",
207
- :space_before => "",
208
- :object_nl => "",
209
- :array_nl => ""
210
- )
211
- else
212
- JSON::State.new(
213
- :indent => " ",
214
- :space => " ",
215
- :space_before => "",
216
- :object_nl => "\n",
217
- :array_nl => "\n"
218
- )
219
- end
220
- @output.write(json_hash.to_json(json_state))
221
- end
222
- end
223
-
224
- ##
225
- # Returns the representation of a IRI reference.
226
- #
227
- # Spec confusion: should a subject IRI be normalized?
228
- #
229
- # @param [RDF::URI] value
230
- # @param [Hash{Symbol => Object}] options
231
- # @option options [:subject, :predicate, :object] position
232
- # Useful when determining how to serialize.
233
- # @option options [RDF::URI] property
234
- # Property for object reference, which can be used to return bare strings
235
- # @return [Object]
236
- def format_iri(value, options = {})
237
- debug {"format_iri(#{options.inspect}, #{value.inspect})"}
238
-
239
- result = context.compact_iri(value, {:depth => @depth}.merge(options))
240
- unless options[:position] != :object || iri_range?(options[:property])
241
- result = {"@id" => result}
242
- end
243
-
244
- debug {"=> #{result.inspect}"}
245
- result
246
- end
247
-
248
- ##
249
- # @param [RDF::Node] value
250
- # @param [Hash{Symbol => Object}] options
251
- # @return [String]
252
- # @raise [NotImplementedError] unless implemented in subclass
253
- # @see {#format\_iri}
254
- def format_node(value, options = {})
255
- format_iri(value, options)
256
- end
257
-
258
- ##
259
- # Returns the representation of a literal.
260
- #
261
- # @param [RDF::Literal, String, #to_s] literal
262
- # @param [Hash{Symbol => Object}] options
263
- # @option options [RDF::URI] property
264
- # Property referencing literal for type coercion
265
- # @return [Object]
266
- def format_literal(literal, options = {})
267
- debug {"format_literal(#{options.inspect}, #{literal.inspect})"}
268
-
269
- value = Hash.new
270
- value['@literal'] = literal.value
271
- value['@type'] = literal.datatype.to_s if literal.has_datatype?
272
- value['@language'] = literal.language.to_s if literal.has_language?
273
-
274
- result = case literal
275
- when RDF::Literal::Boolean, RDF::Literal::Integer, RDF::Literal::Double
276
- literal.object
277
- else
278
- context.compact_value(options[:property], value, {:depth => @depth}.merge(options))
279
- end
280
-
281
- debug {"=> #{result.inspect}"}
282
- result
283
- end
284
-
285
- ##
286
- # Serialize an RDF list
287
- #
288
- # @param [RDF::URI] object
289
- # @param [Hash{Symbol => Object}] options
290
- # @option options [RDF::URI] property
291
- # Property referencing literal for type and list coercion
292
- # @return [Hash{"@list" => Array<Object>}]
293
- def format_list(object, options = {})
294
- predicate = options[:property]
295
- list = RDF::List.new(object, @graph)
296
- ary = []
297
-
298
- debug {"format_list(#{list.inspect}, #{predicate})"}
299
-
300
- depth do
301
- list.each_statement do |st|
302
- next unless st.predicate == RDF.first
303
- debug {" format_list this: #{st.subject} first: #{st.object}"}
304
- ary << if predicate || st.object.literal?
305
- property(predicate, st.object)
306
- else
307
- subject(st.object)
308
- end
309
- subject_done(st.subject)
310
- end
311
- end
312
-
313
- # Returns
314
- ary = {'@list' => ary} unless predicate && list_range?(predicate)
315
- debug {"format_list => #{ary.inspect}"}
316
- ary
317
- end
318
-
319
- private
320
- # Perform any preprocessing of statements required
321
- def preprocess
322
- @graph.each {|statement| preprocess_statement(statement)}
323
- end
324
-
325
- # Perform any statement preprocessing required. This is used to perform reference counts and determine required
326
- # prefixes.
327
- #
328
- # @param [Statement] statement
329
- def preprocess_statement(statement)
330
- debug {"preprocess: #{statement.inspect}"}
331
- references = ref_count(statement.object) + 1
332
- @references[statement.object] = references
333
- @subjects[statement.subject] = true
334
-
335
- depth do
336
- # Pre-fetch qnames, to fill prefixes
337
- format_iri(statement.subject, :position => :subject)
338
- format_iri(statement.predicate, :position => :predicate)
339
-
340
- # To figure out coercion requirements
341
- if statement.object.literal?
342
- format_literal(statement.object, :property => statement.predicate)
343
- datatype_range?(statement.predicate)
344
- else
345
- format_iri(statement.object, :position => :object)
346
- iri_range?(statement.predicate)
347
- end
348
- list_range?(statement.predicate)
349
- end
350
-
351
- @references[statement.predicate] = ref_count(statement.predicate) + 1
352
- end
353
-
354
- # Serialize a subject
355
- # Option contains referencing property, if this is recursive
356
- # @return [Hash]
357
- def subject(subject, options = {})
358
- defn = Hash.new
359
-
360
- raise RDF::WriterError, "Illegal use of subject #{subject.inspect}, not supported" unless subject.resource?
361
-
362
- subject_done(subject)
363
- properties = @graph.properties(subject)
364
- debug {"subject: #{subject.inspect}, props: #{properties.inspect}"}
365
-
366
- @graph.query(:subject => subject).each do |st|
367
- raise RDF::WriterError, "Illegal use of predicate #{st.predicate.inspect}, not supported in RDF/XML" unless st.predicate.uri?
368
- end
369
-
370
- if subject.node? && ref_count(subject) > (options[:property] ? 1 : 0) && options[:expand]
371
- raise RDF::WriterError, "Can't serialize named node when normalizing"
372
- end
373
-
374
- # Subject may be a list
375
- if is_valid_list?(subject)
376
- debug "subject is a list"
377
- defn['@id'] = format_list(subject)
378
- properties.delete(RDF.first.to_s)
379
- properties.delete(RDF.rest.to_s)
380
-
381
- # Special case, if there are no properties, then we can just serialize the list itself
382
- return defn if properties.empty?
383
- elsif subject.uri? || ref_count(subject) > 1
384
- debug "subject is an iri or it's a node referenced multiple times"
385
- # Don't need to set subject if it's a Node without references
386
- defn['@id'] = format_iri(subject, :position => :subject)
387
- else
388
- debug "subject is an unreferenced BNode"
389
- end
390
-
391
- prop_list = order_properties(properties)
392
- debug {"=> property order: #{prop_list.inspect}"}
393
-
394
- prop_list.each do |prop|
395
- predicate = RDF::URI.intern(prop)
396
-
397
- p_iri = format_iri(predicate, :position => :predicate)
398
- depth do
399
- defn[p_iri] = property(predicate, properties[prop])
400
- debug {"prop(#{p_iri}) => #{properties[prop]} => #{defn[p_iri].inspect}"}
401
- end
402
- end
403
-
404
- debug {"subject: #{subject} has defn: #{defn.inspect}"}
405
- defn
406
- end
407
-
408
- ##
409
- # Serialize objects for a property
410
- #
411
- # Spec confusion: sorting of multi-valued properties not adequately specified.
412
- #
413
- # @param [RDF::URI] predicate
414
- # @param [Array<RDF::URI>, RDF::URI] objects
415
- # @param [Hash{Symbol => Object}] options
416
- # @return [Array, Hash, Object]
417
- def property(predicate, objects, options = {})
418
- objects = objects.first if objects.is_a?(Array) && objects.length == 1
419
- case objects
420
- when Array
421
- objects.sort_by(&:to_s).map {|o| property(predicate, o, options)}
422
- when RDF::Literal
423
- format_literal(objects, options.merge(:property => predicate))
424
- else
425
- if is_valid_list?(objects)
426
- format_list(objects, :property => predicate)
427
- elsif is_done?(objects) || !@subjects.include?(objects)
428
- format_iri(objects, :position => :object, :property => predicate)
429
- else
430
- subject(objects, :property => predicate)
431
- end
432
- end
433
- end
434
-
435
- ##
436
- # Take a hash from predicate IRIs to lists of values.
437
- # Sort the lists of values. Return a sorted list of properties.
438
- # @param [Hash{String => Array<Resource>}] properties A hash of Property to Resource mappings
439
- # @return [Array<String>}] Ordered list of properties.
440
- def order_properties(properties)
441
- # Make sorted list of properties
442
- prop_list = []
443
-
444
- properties.keys.sort do |a, b|
445
- format_iri(a, :position => :predicate) <=> format_iri(b, :position => :predicate)
446
- end.each do |prop|
447
- prop_list << prop.to_s
448
- end
449
-
450
- prop_list
451
- end
452
-
453
- # Order subjects for output. Override this to output subjects in another order.
454
- #
455
- # @return [Array<Resource>] Ordered list of subjects
456
- def order_subjects
457
- seen = {}
458
- subjects = []
459
-
460
- return @subjects.keys.sort do |a,b|
461
- format_iri(a, :position => :subject) <=> format_iri(b, :position => :subject)
462
- end unless @options[:automatic]
463
-
464
- # Sort subjects by resources over bnodes, ref_counts and the subject URI itself
465
- recursable = @subjects.keys.
466
- select {|s| !seen.include?(s)}.
467
- map {|r| [r.is_a?(RDF::Node) ? 1 : 0, ref_count(r), r]}.
468
- sort
469
-
470
- subjects += recursable.map{|r| r.last}
471
- end
472
-
473
- # Return the number of times this node has been referenced in the object position
474
- # @return [Integer]
475
- def ref_count(node)
476
- @references.fetch(node, 0)
477
- end
478
-
479
- ##
480
- # Does predicate have a range of IRI?
481
- # @param [RDF::URI] predicate
482
- # @return [Boolean]
483
- def iri_range?(predicate)
484
- return false if predicate.nil? || [RDF.first, RDF.rest].include?(predicate) || @options[:expand]
485
- return true if predicate == RDF.type
486
-
487
- unless context.coerce(predicate)
488
- not_iri = !@options[:automatic]
489
- #debug {" (automatic) = #{(!not_iri).inspect}"}
490
-
491
- # Any literal object makes it not so
492
- not_iri ||= @graph.query(:predicate => predicate).to_a.any? do |st|
493
- l = RDF::List.new(st.object, @graph)
494
- #debug {" o.literal? #{st.object.literal?.inspect}"}
495
- #debug {" l.valid? #{l.valid?.inspect}"}
496
- #debug {" l.any.valid? #{l.to_a.any?(&:literal?).inspect}"}
497
- st.object.literal? || (l.valid? && l.to_a.any?(&:literal?))
498
- end
499
- #debug {" (literal) = #{(!not_iri).inspect}"}
500
-
501
- # FIXME: detect when values are all represented through chaining
502
-
503
- context.coerce(predicate, not_iri ? false : '@id')
147
+ # Turn graph into a triple array
148
+ statements = @graph.each_statement.to_a
149
+ debug("writer") { "serialize #{statements.length} statements, #{@options.inspect}"}
150
+ result = API.fromRDF(statements, nil, @options)
151
+
152
+ # If we were provided a context, or prefixes, use them to compact the output
153
+ context = RDF::Util::File.open_file(@options[:context]) if @options[:context].is_a?(String)
154
+ context ||= @options[:context]
155
+ context ||= if @options[:prefixes] || @options[:language] || @options[:standard_prefixes]
156
+ ctx = EvaluationContext.new(@options)
157
+ ctx.language = @options[:language] if @options[:language]
158
+ @options[:prefixes].each do |prefix, iri|
159
+ ctx.set_mapping(prefix, iri) if prefix && iri
160
+ end if @options[:prefixes]
161
+ ctx
504
162
  end
505
163
 
506
- debug {"iri_range(#{predicate}) = #{context.coerce(predicate).inspect}"}
507
- context.coerce(predicate) == '@id'
508
- end
509
-
510
- ##
511
- # Does predicate have a range of specific typed literal?
512
- # @param [RDF::URI] predicate
513
- # @return [Boolean]
514
- def datatype_range?(predicate)
515
- unless context.coerce(predicate)
516
- # objects of all statements with the predicate must be literal
517
- # and have the same non-nil datatype
518
- dt = nil
519
- if @options[:automatic]
520
- @graph.query(:predicate => predicate) do |st|
521
- debug {"datatype_range? literal? #{st.object.literal?.inspect} dt? #{(st.object.literal? && st.object.has_datatype?).inspect}"}
522
- if st.object.literal? && st.object.has_datatype?
523
- dt = st.object.datatype.to_s if dt.nil?
524
- debug {"=> dt: #{st.object.datatype}"}
525
- dt = false unless dt == st.object.datatype.to_s
526
- else
527
- dt = false
528
- end
529
- end
530
- # Cause necessary prefixes to be output
531
- format_iri(dt, :position => :datatype) if dt && !NATIVE_DATATYPES.include?(dt.to_s)
532
- debug {"range(#{predicate}) = #{dt.inspect}"}
533
- else
534
- dt = false
535
- end
536
- context.coerce(predicate, dt)
164
+ # Perform compaction, if we have a context
165
+ if context
166
+ debug("writer") { "compact result"}
167
+ result = API.compact(result, context, nil, @options)
537
168
  end
538
169
 
539
- context.coerce(predicate)
540
- end
541
-
542
- ##
543
- # Is every use of the predicate an RDF Collection?
544
- #
545
- # @param [RDF::URI] predicate
546
- # @return [Boolean]
547
- def list_range?(predicate)
548
- return false if [RDF.first, RDF.rest].include?(predicate)
549
-
550
- unless @list_range.include?(predicate.to_s)
551
- # objects of all statements with the predicate must be a list
552
- @list_range[predicate.to_s] = if @options[:automatic]
553
- @graph.query(:predicate => predicate).to_a.all? do |st|
554
- is_valid_list?(st.object)
555
- end
556
- else
557
- false
558
- end
559
- context.list(predicate, true) if @list_range[predicate.to_s]
560
-
561
- debug {"list(#{predicate}) = #{@list_range[predicate.to_s].inspect}"}
562
- end
563
-
564
- @list_range[predicate.to_s]
565
- end
566
-
567
- # Reset internal helper instance variables
568
- def reset
569
- @depth = 0
570
- @references = {}
571
- @serialized = {}
572
- @subjects = {}
573
- @list_range = {}
574
- end
575
-
576
- # Checks if l is a valid RDF list, i.e. no nodes have other properties.
577
- def is_valid_list?(l)
578
- #debug {"is_valid_list: #{l.inspect}"}
579
- return RDF::List.new(l, @graph).valid?
580
- end
581
-
582
- def is_done?(subject)
583
- @serialized.include?(subject)
584
- end
585
-
586
- # Mark a subject as done.
587
- def subject_done(subject)
588
- @serialized[subject] = true
589
- end
590
-
591
- # Add debug event to debug array, if specified
592
- #
593
- # @param [String] message
594
- # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
595
- def debug(*args)
596
- return unless ::JSON::LD.debug? || @options[:debug]
597
- message = " " * @depth * 2 + (args.empty? ? "" : args.join(": "))
598
- message += yield if block_given?
599
- puts message if JSON::LD::debug?
600
- @options[:debug] << message if @options[:debug].is_a?(Array)
601
- end
602
-
603
- # Increase depth around a method invocation
604
- def depth
605
- @depth += 1
606
- ret = yield
607
- @depth -= 1
608
- ret
170
+ @output.write(result.to_json(JSON_STATE))
609
171
  end
610
172
  end
611
173
  end