json-ld 0.0.3 → 0.0.4

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.4
@@ -21,11 +21,61 @@ module JSON
21
21
  # @author [Gregg Kellogg](http://greggkellogg.net/)
22
22
  module LD
23
23
  require 'json'
24
+ require 'json/ld/extensions'
24
25
  require 'json/ld/format'
25
26
  autoload :Reader, 'json/ld/reader'
26
27
  autoload :VERSION, 'json/ld/version'
27
28
  autoload :Writer, 'json/ld/writer'
28
29
 
30
+ # Keywords
31
+ BASE = '@base'.freeze
32
+ COERCE = '@coerce'.freeze
33
+ CONTEXT = '@context'.freeze
34
+ DATATYPE = '@datatype'.freeze
35
+ IRI = '@iri'.freeze
36
+ LANGUAGE = '@language'.freeze
37
+ LITERAL = '@literal'.freeze
38
+ SUBJECT = '@subject'.freeze
39
+ TYPE = '@type'.freeze
40
+ VOCAB = '@vocab'.freeze
41
+
42
+ # Default context
43
+ # @see http://json-ld.org/spec/ED/20110507/#the-default-context
44
+ DEFAULT_CONTEXT = {
45
+ 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
46
+ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#',
47
+ 'owl' => 'http://www.w3.org/2002/07/owl#',
48
+ 'xsd' => 'http://www.w3.org/2001/XMLSchema#',
49
+ 'dcterms' => 'http://purl.org/dc/terms/',
50
+ 'foaf' => 'http://xmlns.com/foaf/0.1/',
51
+ 'cal' => 'http://www.w3.org/2002/12/cal/ical#',
52
+ 'vcard' => 'http://www.w3.org/2006/vcard/ns# ',
53
+ 'geo' => 'http://www.w3.org/2003/01/geo/wgs84_pos#',
54
+ 'cc' => 'http://creativecommons.org/ns#',
55
+ 'sioc' => 'http://rdfs.org/sioc/ns#',
56
+ 'doap' => 'http://usefulinc.com/ns/doap#',
57
+ 'com' => 'http://purl.org/commerce#',
58
+ 'ps' => 'http://purl.org/payswarm#',
59
+ 'gr' => 'http://purl.org/goodrelations/v1#',
60
+ 'sig' => 'http://purl.org/signature#',
61
+ 'ccard' => 'http://purl.org/commerce/creditcard#',
62
+ '@coerce' => {
63
+ # Note: rdf:type is not in the document, but necessary for this implementation
64
+ 'xsd:anyURI' => ['rdf:type', 'rdf:rest', 'foaf:homepage', 'foaf:member'],
65
+ 'xsd:integer' => 'foaf:age',
66
+ }
67
+ }.freeze
68
+
69
+ # Default type coercion, in property => datatype order
70
+ DEFAULT_COERCE = {
71
+ RDF.type => RDF::XSD.anyURI,
72
+ RDF.first => false, # Make sure @coerce isn't generated for this
73
+ RDF.rest => RDF::XSD.anyURI,
74
+ RDF::FOAF.homepage => RDF::XSD.anyURI,
75
+ RDF::FOAF.member => RDF::XSD.anyURI,
76
+ RDF::FOAF.age => RDF::XSD.integer,
77
+ }.freeze
78
+
29
79
  def self.debug?; @debug; end
30
80
  def self.debug=(value); @debug = value; end
31
81
  end
@@ -0,0 +1,34 @@
1
+ module RDF
2
+ class Graph
3
+ # Resource properties
4
+ #
5
+ # Properties arranged as a hash with the predicate Term as index to an array of resources or literals
6
+ #
7
+ # Example:
8
+ # graph.load(':foo a :bar; rdfs:label "An example" .', "http://example.com/")
9
+ # graph.resources(URI.new("http://example.com/subject")) =>
10
+ # {
11
+ # "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" => [<http://example.com/#bar>],
12
+ # "http://example.com/#label" => ["An example"]
13
+ # }
14
+ def properties(subject, recalc = false)
15
+ @properties ||= {}
16
+ @properties.delete(subject.to_s) if recalc
17
+ @properties[subject.to_s] ||= begin
18
+ hash = Hash.new
19
+ self.query(:subject => subject) do |statement|
20
+ pred = statement.predicate.to_s
21
+
22
+ hash[pred] ||= []
23
+ hash[pred] << statement.object
24
+ end
25
+ hash
26
+ end
27
+ end
28
+
29
+ # Get type(s) of subject, returns a list of symbols
30
+ def type_of(subject)
31
+ query(:subject => subject, :predicate => RDF.type).map {|st| st.object}
32
+ end
33
+ end
34
+ end
@@ -28,4 +28,20 @@ module JSON::LD
28
28
  reader { JSON::LD::Reader }
29
29
  writer { JSON::LD::Writer }
30
30
  end
31
+
32
+ # Alias for JSON-LD format
33
+ #
34
+ # This allows the following:
35
+ #
36
+ # @example Obtaining an Notation3 format class
37
+ # RDF::Format.for(:jsonld) #=> JSON::LD::JSONLD
38
+ # RDF::Format.for(:jsonld).reader #=> JSON::LD::Reader
39
+ # RDF::Format.for(:jsonld).writer #=> JSON::LD::Writer
40
+ class JSONLD < RDF::Format
41
+ content_type 'application/json', :extension => :jsonld
42
+ content_encoding 'utf-8'
43
+
44
+ reader { JSON::LD::Reader }
45
+ writer { JSON::LD::Writer }
46
+ end
31
47
  end
@@ -7,33 +7,6 @@ module JSON::LD
7
7
  class Reader < RDF::Reader
8
8
  format Format
9
9
 
10
- # Default context
11
- # @see http://json-ld.org/spec/ED/20110507/#the-default-context
12
- DEFAULT_CONTEXT = {
13
- "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
14
- "rdfs" => "http://www.w3.org/2000/01/rdf-schema#",
15
- "owl" => "http://www.w3.org/2002/07/owl#",
16
- "xsd" => "http://www.w3.org/2001/XMLSchema#",
17
- "dcterms" => "http://purl.org/dc/terms/",
18
- "foaf" => "http://xmlns.com/foaf/0.1/",
19
- "cal" => "http://www.w3.org/2002/12/cal/ical#",
20
- "vcard" => "http://www.w3.org/2006/vcard/ns# ",
21
- "geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
22
- "cc" => "http://creativecommons.org/ns#",
23
- "sioc" => "http://rdfs.org/sioc/ns#",
24
- "doap" => "http://usefulinc.com/ns/doap#",
25
- "com" => "http://purl.org/commerce#",
26
- "ps" => "http://purl.org/payswarm#",
27
- "gr" => "http://purl.org/goodrelations/v1#",
28
- "sig" => "http://purl.org/signature#",
29
- "ccard" => "http://purl.org/commerce/creditcard#",
30
- "@coerce" => {
31
- # Note: rdf:type is not in the document, but necessary for this implementation
32
- "xsd:anyURI" => ["rdf:type", "foaf:homepage", "foaf:member", "rdf:type"],
33
- "xsd:integer" => "foaf:age",
34
- }
35
- }.freeze
36
-
37
10
  ##
38
11
  # The graph constructed when parsing.
39
12
  #
@@ -181,44 +154,49 @@ module JSON::LD
181
154
 
182
155
  # 2.1) If a @context keyword is found, the processor merges each key-value pair in
183
156
  # the local context into the active context ...
184
- if element["@context"]
157
+ if element[CONTEXT]
185
158
  # Merge context
186
- ec = parse_context(ec.dup, element["@context"])
159
+ ec = parse_context(ec.dup, element[CONTEXT])
187
160
  prefixes.merge!(ec.mappings) # Update parsed prefixes
188
161
  end
189
162
 
190
163
  # Other shortcuts to allow use of this method for terminal associative arrays
191
- if element["@iri"].is_a?(String)
164
+ if element[IRI].is_a?(String)
192
165
  # Return the IRI found from the value
193
- object = expand_term(element["@iri"], ec.base, ec)
166
+ object = expand_term(element[IRI], ec.base, ec)
194
167
  add_triple(path, subject, property, object) if subject && property
195
- elsif element["@literal"]
168
+ return
169
+ elsif element[LITERAL]
196
170
  literal_opts = {}
197
- literal_opts[:datatype] = expand_term(element["@datatype"], ec.vocab.to_s, ec) if element["@datatype"]
198
- literal_opts[:language] = element["@language"].to_sym if element["@language"]
199
- object = RDF::Literal.new(element["@literal"], literal_opts)
171
+ literal_opts[:datatype] = expand_term(element[DATATYPE], ec.vocab.to_s, ec) if element[DATATYPE]
172
+ literal_opts[:language] = element[LANGUAGE].to_sym if element[LANGUAGE]
173
+ object = RDF::Literal.new(element[LITERAL], literal_opts)
200
174
  add_triple(path, subject, property, object) if subject && property
175
+ return
201
176
  end
202
177
 
203
178
  # 2.2) ... Otherwise, if the local context is known perform the following steps:
204
179
  # 2.2.1) If a @ key is found, the processor sets the active subject to the
205
180
  # value after Object Processing has been performed.
206
- if element["@"].is_a?(String)
207
- active_subject = expand_term(element["@"], ec.base, ec)
208
-
209
- # 2.2.1.1) If the inherited subject and inherited property values are
210
- # specified, generate a triple using the inherited subject for the
211
- # subject, the inherited property for the property, and the active
212
- # subject for the object.
213
- add_triple(path, subject, property, active_subject) if subject && property
214
-
215
- subject = active_subject
181
+ if element[SUBJECT].is_a?(String)
182
+ active_subject = expand_term(element[SUBJECT], ec.base, ec)
183
+ elsif element[SUBJECT]
184
+ # Recursively process hash or Array values
185
+ traverse("#{path}[@]", element[SUBJECT], subject, property, ec)
216
186
  else
217
187
  # 2.2.7) If the end of the associative array is detected, and a active subject
218
188
  # was not discovered, then:
219
189
  # 2.2.7.1) Generate a blank node identifier and set it as the active subject.
220
- subject = RDF::Node.new
190
+ active_subject = RDF::Node.new
221
191
  end
192
+
193
+ # 2.2.1.1) If the inherited subject and inherited property values are
194
+ # specified, generate a triple using the inherited subject for the
195
+ # subject, the inherited property for the property, and the active
196
+ # subject for the object.
197
+ # 2.2.7.2) Complete any previously incomplete triples by running all substeps of Step 2.2.1.
198
+ add_triple(path, subject, property, active_subject) if subject && property
199
+ subject = active_subject
222
200
 
223
201
  element.each do |key, value|
224
202
  # 2.2.3) If a key that is not @context, @, or a, set the active property by
@@ -319,23 +297,12 @@ module JSON::LD
319
297
  # @raise [RDF::ReaderError] on a syntax error, or a reference to a term which is not defined.
320
298
  def parse_context(ec, context)
321
299
  context.each do |key, value|
322
- #add_debug("parse_context(#{key})", value.inspect)
300
+ add_debug("parse_context(#{key})", value.inspect)
323
301
  case key
324
- when '@vocab' then ec.vocab = value
325
- when '@base' then ec.base = uri(value)
326
- when '@coerce'
327
- # Spec confusion: doc says to merge each key-value mapping to the local context's @coerce mapping,
328
- # overwriting duplicate values. In the case where a mapping is indicated to a list of properties
329
- # (e.g., { "xsd:anyURI": ["foaf:homepage", "foaf:member"] }, does this overwrite a previous mapping
330
- # of { "xsd:anyURI": "foaf:knows" }, or add to it.
331
- add_error RDF::ReaderError, "Expected @coerce to reference an associative array" unless value.is_a?(Hash)
332
- value.each do |type, property|
333
- type_uri = expand_term(type, ec.vocab, ec)
334
- [property].flatten.compact.each do |prop|
335
- p = expand_term(prop, ec.vocab, ec)
336
- ec.coerce[p] = type_uri
337
- end
338
- end
302
+ when VOCAB then ec.vocab = value
303
+ when BASE then ec.base = uri(value)
304
+ when COERCE
305
+ # Process after prefix mapping
339
306
  else
340
307
  # Spec confusion: The text indicates to merge each key-value pair into the active context. Is any
341
308
  # processing performed on the values. For instance, could a value be a CURIE, or {"@iri": <value>}?
@@ -345,6 +312,21 @@ module JSON::LD
345
312
  end
346
313
  end
347
314
 
315
+ if context[COERCE]
316
+ # Spec confusion: doc says to merge each key-value mapping to the local context's @coerce mapping,
317
+ # overwriting duplicate values. In the case where a mapping is indicated to a list of properties
318
+ # (e.g., { "xsd:anyURI": ["foaf:homepage", "foaf:member"] }, does this overwrite a previous mapping
319
+ # of { "xsd:anyURI": "foaf:knows" }, or add to it.
320
+ add_error RDF::ReaderError, "Expected @coerce to reference an associative array" unless context[COERCE].is_a?(Hash)
321
+ context[COERCE].each do |type, property|
322
+ type_uri = expand_term(type, ec.vocab, ec)
323
+ [property].flatten.compact.each do |prop|
324
+ p = expand_term(prop, ec.vocab, ec)
325
+ ec.coerce[p] = type_uri
326
+ end
327
+ end
328
+ end
329
+
348
330
  ec
349
331
  end
350
332
 
@@ -2,10 +2,662 @@ module JSON::LD
2
2
  ##
3
3
  # A JSON-LD parser in Ruby.
4
4
  #
5
+ # Note that the natural interface is to write a whole graph at a time.
6
+ # Writing statements or Triples will create a graph to add them to
7
+ # and then serialize the graph.
8
+ #
9
+ # @example Obtaining a JSON-LD writer class
10
+ # RDF::Writer.for(:jsonld) #=> RDF::N3::Writer
11
+ # RDF::Writer.for("etc/test.json")
12
+ # RDF::Writer.for(:file_name => "etc/test.json")
13
+ # RDF::Writer.for(:file_extension => "json")
14
+ # RDF::Writer.for(:content_type => "application/turtle")
15
+ #
16
+ # @example Serializing RDF graph into an JSON-LD file
17
+ # JSON::LD::Writer.open("etc/test.json") do |writer|
18
+ # writer << graph
19
+ # end
20
+ #
21
+ # @example Serializing RDF statements into an JSON-LD file
22
+ # JSON::LD::Writer.open("etc/test.json") do |writer|
23
+ # graph.each_statement do |statement|
24
+ # writer << statement
25
+ # end
26
+ # end
27
+ #
28
+ # @example Serializing RDF statements into an JSON-LD string
29
+ # JSON::LD::Writer.buffer do |writer|
30
+ # graph.each_statement do |statement|
31
+ # writer << statement
32
+ # end
33
+ # end
34
+ #
35
+ # The writer will add prefix definitions, and use them for creating @context definitions, and minting CURIEs
36
+ #
37
+ # @example Creating @base, @vocab and @context prefix definitions in output
38
+ # JSON::LD::Writer.buffer(
39
+ # :base_uri => "http://example.com/",
40
+ # :vocab => "http://example.net/"
41
+ # :prefixes => {
42
+ # nil => "http://example.com/ns#",
43
+ # :foaf => "http://xmlns.com/foaf/0.1/"}
44
+ # ) do |writer|
45
+ # graph.each_statement do |statement|
46
+ # writer << statement
47
+ # end
48
+ # end
49
+ #
50
+ # Select the :canonicalize option to output JSON-LD in canonical form
51
+ #
5
52
  # @see http://json-ld.org/spec/ED/20110507/
53
+ # @see http://json-ld.org/spec/ED/20110507/#the-normalization-algorithm
6
54
  # @author [Gregg Kellogg](http://greggkellogg.net/)
7
55
  class Writer < RDF::Writer
8
56
  format Format
57
+
58
+ # @attr [Graph] Graph of statements serialized
59
+ attr :graph
60
+ # @attr [URI] Base IRI used for relativizing IRIs
61
+ attr :base_uri
62
+ # @attr [String] Vocabulary prefix used for relativizing IRIs
63
+ attr :vocab
64
+
65
+ # Type coersion to use for serialization. Defaults to DEFAULT_COERCION
66
+ #
67
+ # Maintained as a reverse mapping of `property` => `type`.
68
+ #
69
+ # @attr [Hash{RDF::URI => RDF::URI}]
70
+ attr :coerce, true
71
+
72
+ ##
73
+ # Return the pre-serialized Hash before turning into JSON
74
+ #
75
+ # @return [Hash]
76
+ def self.hash(*args, &block)
77
+ hash = {}
78
+ self.new(hash, *args, &block)
79
+ hash
80
+ end
81
+
82
+ ##
83
+ # Initializes the RDF-LD writer instance.
84
+ #
85
+ # @param [IO, File] output
86
+ # the output stream
87
+ # @param [Hash{Symbol => Object}] options
88
+ # any additional options
89
+ # @option options [Encoding] :encoding (Encoding::UTF_8)
90
+ # the encoding to use on the output stream (Ruby 1.9+)
91
+ # @option options [Boolean] :canonicalize (false)
92
+ # whether to canonicalize literals when serializing
93
+ # @option options [Hash] :prefixes (Hash.new)
94
+ # the prefix mappings to use (not supported by all writers)
95
+ # @option options [#to_s] :base_uri (nil)
96
+ # Base IRI used for relativizing IRIs
97
+ # @option options [#to_s] :vocab (nil)
98
+ # Vocabulary prefix used for relativizing IRIs
99
+ # @option options [Boolean] :standard_prefixes (false)
100
+ # Add standard prefixes to @prefixes, if necessary.
101
+ # @yield [writer] `self`
102
+ # @yieldparam [RDF::Writer] writer
103
+ # @yieldreturn [void]
104
+ # @yield [writer]
105
+ # @yieldparam [RDF::Writer] writer
106
+ def initialize(output = $stdout, options = {}, &block)
107
+ super do
108
+ @graph = RDF::Graph.new
109
+ @iri_to_prefix = DEFAULT_CONTEXT.dup.delete_if {|k,v| k == COERCE}.invert
110
+ @coerce = DEFAULT_COERCE.merge(options[:coerce] || {})
111
+ if block_given?
112
+ case block.arity
113
+ when 0 then instance_eval(&block)
114
+ else block.call(self)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ ##
121
+ # Write whole graph
122
+ #
123
+ # @param [Graph] graph
124
+ # @return [void]
125
+ def write_graph(graph)
126
+ add_debug "Add graph #{graph.inspect}"
127
+ @graph = graph
128
+ end
129
+
130
+ ##
131
+ # Addes a statement to be serialized
132
+ # @param [RDF::Statement] statement
133
+ # @return [void]
134
+ def write_statement(statement)
135
+ @graph.insert(statement)
136
+ end
137
+
138
+ ##
139
+ # Addes a triple to be serialized
140
+ # @param [RDF::Resource] subject
141
+ # @param [RDF::URI] predicate
142
+ # @param [RDF::Value] object
143
+ # @return [void]
144
+ # @raise [NotImplementedError] unless implemented in subclass
145
+ # @abstract
146
+ def write_triple(subject, predicate, object)
147
+ @graph.insert(Statement.new(subject, predicate, object))
148
+ end
149
+
150
+ ##
151
+ # Outputs the Serialized JSON-LD representation of all stored triples.
152
+ #
153
+ # @return [void]
154
+ # @see #write_triple
155
+ def write_epilogue
156
+ @base_uri = RDF::URI(@options[:base_uri]) if @options[:base_uri] && !@options[:canonicalize]
157
+ @vocab = @options[:vocab] unless @options[:canonicalize]
158
+ @debug = @options[:debug]
159
+
160
+ reset
161
+
162
+ add_debug "\nserialize: graph: #{@graph.size}"
163
+
164
+ preprocess
165
+
166
+ # Don't generate context for canonical output
167
+ json_hash = @options[:canonicalize] ? {} : start_document
168
+
169
+ elements = []
170
+ order_subjects.each do |subject|
171
+ unless is_done?(subject)
172
+ elements << subject(subject, json_hash)
173
+ end
174
+ end
175
+
176
+ return if elements.empty?
177
+
178
+ if elements.length == 1 && elements.first.is_a?(Hash)
179
+ json_hash.merge!(elements.first)
180
+ else
181
+ json_hash[SUBJECT] = elements
182
+ end
183
+
184
+ if @output.is_a?(Hash)
185
+ @output.merge!(json_hash)
186
+ else
187
+ json_state = if @options[:canonicalize]
188
+ JSON::State.new(
189
+ :indent => "",
190
+ :space => "",
191
+ :space_before => "",
192
+ :object_nl => "",
193
+ :array_nl => ""
194
+ )
195
+ else
196
+ JSON::State.new(
197
+ :indent => " ",
198
+ :space => " ",
199
+ :space_before => "",
200
+ :object_nl => "\n",
201
+ :array_nl => "\n"
202
+ )
203
+ end
204
+ @output.write(json_hash.to_json(json_state))
205
+ end
206
+ end
207
+
208
+ ##
209
+ # Returns the representation of a IRI reference.
210
+ #
211
+ # Spec confusion: should a subject URI be normalized?
212
+ #
213
+ # @param [RDF::URI] value
214
+ # @param [Hash{Symbol => Object}] options
215
+ # @option options [:subject, :predicate, :object] position
216
+ # Useful when determining how to serialize.
217
+ # @option options [RDF::URI] property
218
+ # Property for object reference, which can be used to return
219
+ # bare strings, rather than {"iri":}
220
+ # @return [Object]
221
+ def format_uri(value, options = {})
222
+ result = case options[:position]
223
+ when :subject
224
+ # attempt base_uri replacement
225
+ short = value.to_s.sub(base_uri.to_s, "")
226
+ short == value.to_s ? (get_curie(value) || value.to_s) : short
227
+ when :predicate
228
+ # attempt vocab replacement
229
+ short = TYPE if value == RDF.type
230
+ short ||= value.to_s.sub(@vocab.to_s, "")
231
+ short == value.to_s ? (get_curie(value) || value.to_s) : short
232
+ else
233
+ # Encode like a subject
234
+ iri_range?(options[:property]) ?
235
+ format_uri(value, :position => :subject) :
236
+ {:iri => format_uri(value, :position => :subject)}
237
+ end
238
+
239
+ add_debug("format_uri(#{options.inspect}, #{value.inspect}) => #{result.inspect}")
240
+ result
241
+ end
242
+
243
+ ##
244
+ # @param [RDF::Node] value
245
+ # @param [Hash{Symbol => Object}] options
246
+ # @return [String]
247
+ # @raise [NotImplementedError] unless implemented in subclass
248
+ # @abstract
249
+ def format_node(value, options = {})
250
+ format_uri(value, options)
251
+ end
252
+
253
+ ##
254
+ # Returns the representation of a literal.
255
+ #
256
+ # @param [RDF::Literal, String, #to_s] literal
257
+ # @param [Hash{Symbol => Object}] options
258
+ # @option options [RDF::URI] property
259
+ # Property referencing literal for type coercion
260
+ # @return [Object]
261
+ def format_literal(literal, options = {})
262
+ if options[:canonical] || @options[:canonicalize]
263
+ return {
264
+ :literal => literal.value,
265
+ :datatype => (format_uri(literal.datatype, :position => :subject) if literal.has_datatype?),
266
+ :language => (literal.language.to_s if literal.has_language?)
267
+ }.delete_if {|k,v| v.nil?}
268
+ end
269
+
270
+ case literal
271
+ when RDF::Literal::Integer, RDF::Literal::Boolean
272
+ literal.object
273
+ when RDF::Literal
274
+ if datatype_range?(options[:property]) || !(literal.has_datatype? || literal.has_language?)
275
+ # Datatype coercion where literal has the same datatype
276
+ literal.value
277
+ else
278
+ format_literal(literal, :canonical => true)
279
+ end
280
+ end
281
+ end
282
+
283
+ ##
284
+ # Serialize an RDF list
285
+ # @param [RDF::URI] object
286
+ # @param [Hash{Symbol => Object}] options
287
+ # @option options [RDF::URI] property
288
+ # Property referencing literal for type coercion
289
+ # @return [Array<Array<Object>>]
290
+ def format_list(object, options = {})
291
+ predicate = options[:property]
292
+ list = []
293
+
294
+ add_debug "format_list(#{object}, #{predicate})"
295
+
296
+ @depth += 1
297
+ while object do
298
+ subject_done(object)
299
+ p = @graph.properties(object)
300
+ item = p.fetch(RDF.first.to_s, []).first
301
+ if item
302
+ add_debug "format_list serialize #{item.inspect}"
303
+ list << if predicate || item.literal?
304
+ property(predicate, item)
305
+ else
306
+ subject(item)
307
+ end
308
+ end
309
+ object = p.fetch(RDF.rest.to_s, []).first
310
+ end
311
+ @depth -= 1
312
+
313
+ # Returns
314
+ add_debug "format_list => #{[list].inspect}"
315
+ [list]
316
+ end
317
+
318
+ private
319
+ ##
320
+ # Generate @context
321
+ # @return [Hash]
322
+ def start_document
323
+ ctx = {}
324
+ ctx[BASE] = base_uri.to_s if base_uri
325
+ ctx[VOCAB] = vocab.to_s if vocab
326
+
327
+ # Prefixes
328
+ prefixes.keys.sort {|a,b| a.to_s <=> b.to_s}.each do |k|
329
+ next if DEFAULT_CONTEXT.has_key?(k.to_s)
330
+ add_debug "prefix[#{k}] => #{prefixes[k]}"
331
+ ctx[k.to_s] = prefixes[k].to_s
332
+ end
333
+
334
+ # Coerce
335
+ add_debug "start_doc: coerce= #{coerce.inspect}"
336
+ unless coerce == DEFAULT_COERCE
337
+ c_h = {}
338
+ coerce.keys.sort.each do |k|
339
+ next if coerce[k] == DEFAULT_COERCE[k] ||
340
+ coerce[k] == false ||
341
+ coerce[k] == RDF::XSD.integer ||
342
+ coerce[k] == RDF::XSD.boolean
343
+ k_iri = format_uri(k, :position => :predicate)
344
+ d_iri = format_uri(coerce[k], :position => :subject)
345
+ add_debug "coerce[#{k_iri}] => #{d_iri}"
346
+ case c_h[d_iri]
347
+ when nil
348
+ c_h[d_iri] = k_iri
349
+ when Array
350
+ c_h[d_iri] << k_iri
351
+ else
352
+ c_h[d_iri] = [c_h[d_iri], k_iri]
353
+ end
354
+ end
355
+
356
+ ctx[COERCE] = c_h unless c_h.empty?
357
+ end
358
+
359
+ add_debug "start_doc: context=#{ctx.inspect}"
360
+ # Return hash with @context, or empty
361
+ ctx.empty? ? {} : {CONTEXT => ctx}
362
+ end
363
+
364
+ # Perform any preprocessing of statements required
365
+ def preprocess
366
+ # Load defined prefixes
367
+ (@options[:prefixes] || {}).each_pair do |k, v|
368
+ @iri_to_prefix[v.to_s] = k
369
+ end
370
+ @options[:prefixes] = {} # Will define actual used when matched
371
+
372
+ @graph.each {|statement| preprocess_statement(statement)}
373
+ end
374
+
375
+ # Perform any statement preprocessing required. This is used to perform reference counts and determine required
376
+ # prefixes.
377
+ # @param [Statement] statement
378
+ def preprocess_statement(statement)
379
+ add_debug "preprocess: #{statement.inspect}"
380
+ references = ref_count(statement.object) + 1
381
+ @references[statement.object] = references
382
+ @subjects[statement.subject] = true
383
+
384
+ # Pre-fetch qnames, to fill prefixes
385
+ get_curie(statement.subject)
386
+ get_curie(statement.predicate)
387
+ if statement.object.literal?
388
+ datatype_range?(statement.predicate) # To figure out coercion requirements
389
+ else
390
+ iri_range?(statement.predicate)
391
+ get_curie(statement.object)
392
+ end
393
+
394
+ @references[statement.predicate] = ref_count(statement.predicate) + 1
395
+ end
396
+
397
+ # Serialize a subject
398
+ # Option contains referencing property, if this is recursive
399
+ # @return [Hash]
400
+ def subject(subject, options = {})
401
+ defn = {}
402
+
403
+ raise RDF::WriterError, "Illegal use of subject #{subject.inspect}, not supported" unless subject.resource?
404
+
405
+ subject_done(subject)
406
+ properties = @graph.properties(subject)
407
+ add_debug "subject: #{subject.inspect}, props: #{properties.inspect}"
408
+
409
+ @graph.query(:subject => subject).each do |st|
410
+ raise RDF::WriterError, "Illegal use of predicate #{st.predicate.inspect}, not supported in RDF/XML" unless st.predicate.uri?
411
+ end
412
+
413
+ if subject.node? && ref_count(subject) > (options[:property] ? 1 : 0) && options[:canonicalize]
414
+ raise RDF::WriterError, "Can't serialize named node when normalizing"
415
+ end
416
+
417
+ # Subject may be a list
418
+ if is_valid_list?(subject)
419
+ add_debug "subject is a list"
420
+ defn[SUBJECT] = format_list(subject)
421
+ properties.delete(RDF.first.to_s)
422
+ properties.delete(RDF.rest.to_s)
423
+
424
+ # Special case, if there are no properties, then we can just serialize the list itself
425
+ return defn[SUBJECT].first if properties.empty?
426
+ elsif subject.uri? || ref_count(subject) > 1
427
+ add_debug "subject is a uri"
428
+ # Don't need to set subject if it's a Node without references
429
+ defn[SUBJECT] = format_uri(subject, :position => :subject)
430
+ else
431
+ add_debug "subject is an unreferenced BNode"
432
+ end
433
+
434
+ prop_list = order_properties(properties)
435
+ #add_debug "=> property order: #{prop_list.to_sentence}"
436
+
437
+ prop_list.each do |prop|
438
+ predicate = RDF::URI.intern(prop)
439
+
440
+ p_iri = format_uri(predicate, :position => :predicate)
441
+ @depth += 1
442
+ defn[p_iri] = property(predicate, properties[prop])
443
+ add_debug "prop(#{p_iri}) => #{properties[prop]} => #{defn[p_iri].inspect}"
444
+ @depth -= 1
445
+ end
446
+
447
+ add_debug "subject: #{subject} has defn: #{defn.inspect}"
448
+ defn
449
+ end
450
+
451
+ ##
452
+ # Serialize objects for a property
453
+ #
454
+ # Spec confusion: sorting of multi-valued properties not adequately specified.
455
+ #
456
+ # @param [RDF::URI] predicate
457
+ # @param [Array<RDF::URI>, RDF::URI] objects
458
+ # @param [Hash{Symbol => Object}] options
459
+ # @return [Array, Hash, Object]
460
+ def property(predicate, objects, options = {})
461
+ objects = objects.first if objects.is_a?(Array) && objects.length == 1
462
+ case objects
463
+ when Array
464
+ objects.sort_by(&:to_s).map {|o| property(predicate, o, options)}
465
+ when RDF::Literal
466
+ format_literal(objects, options.merge(:property => predicate))
467
+ else
468
+ if is_valid_list?(objects)
469
+ format_list(objects, :property => predicate)
470
+ elsif is_done?(objects) || !@subjects.include?(objects)
471
+ format_uri(objects, :position => :object, :property => predicate)
472
+ else
473
+ subject(objects, :property => predicate)
474
+ end
475
+ end
476
+ end
477
+
478
+ ##
479
+ # Return a CURIE for the IRI, or nil. Adds namespace of CURIE to defined prefixes
480
+ # @param [RDF::Resource] resource
481
+ # @return [String, nil] value to use to identify IRI
482
+ def get_curie(resource)
483
+ add_debug "get_curie(#{resource.inspect})"
484
+ case resource
485
+ when RDF::Node
486
+ return resource.to_s
487
+ when RDF::URI
488
+ iri = resource.to_s
489
+ return iri if options[:canonicalize]
490
+ else
491
+ return nil
492
+ end
493
+
494
+ curie = case
495
+ when @iri_to_curie.has_key?(iri)
496
+ return @iri_to_curie[iri]
497
+ when u = @iri_to_prefix.keys.detect {|u| iri.index(u.to_s) == 0}
498
+ # Use a defined prefix
499
+ prefix = @iri_to_prefix[u]
500
+ prefix(prefix, u) # Define for output
501
+ iri.sub(u.to_s, "#{prefix}:")
502
+ when @options[:standard_prefixes] && vocab = RDF::Vocabulary.detect {|v| iri.index(v.to_uri.to_s) == 0}
503
+ prefix = vocab.__name__.to_s.split('::').last.downcase
504
+ @iri_to_prefix[vocab.to_uri.to_s] = prefix
505
+ prefix(prefix, vocab.to_uri) # Define for output
506
+ iri.sub(vocab.to_uri.to_s, "#{prefix}:")
507
+ else
508
+ nil
509
+ end
510
+
511
+ @iri_to_curie[iri] = curie
512
+ rescue Addressable::URI::InvalidURIError => e
513
+ raise RDF::WriterError, "Invalid IRI #{resource.inspect}: #{e.message}"
514
+ end
515
+
516
+ ##
517
+ # Take a hash from predicate IRIs to lists of values.
518
+ # Sort the lists of values. Return a sorted list of properties.
519
+ # @param [Hash{String => Array<Resource>}] properties A hash of Property to Resource mappings
520
+ # @return [Array<String>}] Ordered list of properties.
521
+ def order_properties(properties)
522
+ # Make sorted list of properties
523
+ prop_list = []
524
+
525
+ properties.keys.sort do |a,b|
526
+ format_uri(a, :position => :predicate) <=> format_uri(b, :position => :predicate)
527
+ end.each do |prop|
528
+ prop_list << prop.to_s
529
+ end
530
+
531
+ prop_list
532
+ end
533
+
534
+ # Order subjects for output. Override this to output subjects in another order.
535
+ #
536
+ # Uses #base_uri.
537
+ # @return [Array<Resource>] Ordered list of subjects
538
+ def order_subjects
539
+ seen = {}
540
+ subjects = []
541
+
542
+ return @subjects.keys.sort do |a,b|
543
+ format_iri(a, :position => :subject) <=> format_iri(b, :position => :subject)
544
+ end if @options[:canonicalize]
545
+
546
+ # Start with base_uri
547
+ if base_uri && @subjects.keys.include?(base_uri)
548
+ subjects << base_uri
549
+ seen[base_uri] = true
550
+ end
551
+
552
+ # Sort subjects by resources over bnodes, ref_counts and the subject URI itself
553
+ recursable = @subjects.keys.
554
+ select {|s| !seen.include?(s)}.
555
+ map {|r| [r.is_a?(RDF::Node) ? 1 : 0, ref_count(r), r]}.
556
+ sort
557
+
558
+ subjects += recursable.map{|r| r.last}
559
+ end
560
+
561
+ # Return the number of times this node has been referenced in the object position
562
+ # @return [Integer]
563
+ def ref_count(node)
564
+ @references.fetch(node, 0)
565
+ end
566
+
567
+ ##
568
+ # Does predicate have a range of IRI?
569
+ # @param [RDF::URI] predicate
570
+ # @return [Boolean]
571
+ def iri_range?(predicate)
572
+ return false if predicate.nil? || @options[:canonicalize]
573
+
574
+ unless coerce.has_key?(predicate)
575
+ # objects of all statements with the predicate may not be literal
576
+ coerce[predicate] = @graph.query(:predicate => predicate).to_a.any? {|st| st.object.literal?} ?
577
+ false : RDF::XSD.anyURI
578
+ end
579
+
580
+ add_debug "iri_range(#{predicate}) = #{coerce[predicate].inspect}"
581
+ coerce[predicate] == RDF::XSD.anyURI
582
+ end
583
+
584
+ ##
585
+ # Does predicate have a range of specific typed literal?
586
+ # @param [RDF::URI] predicate
587
+ # @return [Boolean]
588
+ def datatype_range?(predicate)
589
+ unless coerce.has_key?(predicate)
590
+ # objects of all statements with the predicate must be literal
591
+ # and have the same non-nil datatype
592
+ dt = nil
593
+ @graph.query(:predicate => predicate) do |st|
594
+ if st.object.literal? && st.object.has_datatype?
595
+ dt = st.object.datatype if dt.nil?
596
+ dt = false unless dt == st.object.datatype
597
+ else
598
+ dt = false
599
+ end
600
+ end
601
+ add_debug "range(#{predicate}) = #{dt.inspect}"
602
+ coerce[predicate] = dt
603
+ end
604
+
605
+ coerce[predicate]
606
+ end
607
+
608
+ # Reset internal helper instance variables
609
+ def reset
610
+ @depth = 0
611
+ @references = {}
612
+ @serialized = {}
613
+ @subjects = {}
614
+ @iri_to_curie = {}
615
+ end
616
+
617
+ # Add debug event to debug array, if specified
618
+ #
619
+ # @param [String] message::
620
+ def add_debug(message)
621
+ msg = "#{" " * @depth * 2}#{message}"
622
+ STDERR.puts msg if ::JSON::LD::debug?
623
+ @debug << msg if @debug.is_a?(Array)
624
+ end
625
+
626
+ # Checks if l is a valid RDF list, i.e. no nodes have other properties.
627
+ def is_valid_list?(l)
628
+ props = @graph.properties(l)
629
+ unless l.node? && props.has_key?(RDF.first.to_s) || l == RDF.nil
630
+ add_debug "is_valid_list: false, #{l.inspect}: #{props.inspect}"
631
+ return false
632
+ end
633
+
634
+ while l && l != RDF.nil do
635
+ #add_debug "is_valid_list(length): #{props.length}"
636
+ return false unless props.has_key?(RDF.first.to_s) && props.has_key?(RDF.rest.to_s)
637
+ n = props[RDF.rest.to_s]
638
+ unless n.is_a?(Array) && n.length == 1
639
+ add_debug "is_valid_list: false, #{n.inspect}"
640
+ return false
641
+ end
642
+ l = n.first
643
+ unless l.node? || l == RDF.nil
644
+ add_debug "is_valid_list: false, #{l.inspect}"
645
+ return false
646
+ end
647
+ props = @graph.properties(l)
648
+ end
649
+ add_debug "is_valid_list: valid"
650
+ true
651
+ end
652
+
653
+ def is_done?(subject)
654
+ @serialized.include?(subject)
655
+ end
656
+
657
+ # Mark a subject as done.
658
+ def subject_done(subject)
659
+ @serialized[subject] = true
660
+ end
9
661
  end
10
662
  end
11
663
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: json-ld
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.3
5
+ version: 0.0.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Gregg Kellogg
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-05-10 00:00:00 -07:00
13
+ date: 2011-06-29 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -19,7 +19,7 @@ dependencies:
19
19
  requirement: &id001 !ruby/object:Gem::Requirement
20
20
  none: false
21
21
  requirements:
22
- - - ~>
22
+ - - ">="
23
23
  - !ruby/object:Gem::Version
24
24
  version: 0.3.3
25
25
  type: :runtime
@@ -63,11 +63,66 @@ dependencies:
63
63
  requirement: &id005 !ruby/object:Gem::Requirement
64
64
  none: false
65
65
  requirements:
66
- - - ~>
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.3.2
69
69
  type: :development
70
70
  version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: rdf-n3
73
+ prerelease: false
74
+ requirement: &id006 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 0.3.3
80
+ type: :development
81
+ version_requirements: *id006
82
+ - !ruby/object:Gem::Dependency
83
+ name: rdf-isomorphic
84
+ prerelease: false
85
+ requirement: &id007 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 0.3.4
91
+ type: :development
92
+ version_requirements: *id007
93
+ - !ruby/object:Gem::Dependency
94
+ name: sxp
95
+ prerelease: false
96
+ requirement: &id008 !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: "0"
102
+ type: :development
103
+ version_requirements: *id008
104
+ - !ruby/object:Gem::Dependency
105
+ name: sparql-algebra
106
+ prerelease: false
107
+ requirement: &id009 !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: "0"
113
+ type: :development
114
+ version_requirements: *id009
115
+ - !ruby/object:Gem::Dependency
116
+ name: sparql-grammar
117
+ prerelease: false
118
+ requirement: &id010 !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: "0"
124
+ type: :development
125
+ version_requirements: *id010
71
126
  description: JSON-LD reader/writer for Ruby.
72
127
  email: public-rdf-ruby@w3.org
73
128
  executables: []
@@ -81,6 +136,7 @@ files:
81
136
  - README
82
137
  - UNLICENSE
83
138
  - VERSION
139
+ - lib/json/ld/extensions.rb
84
140
  - lib/json/ld/format.rb
85
141
  - lib/json/ld/reader.rb
86
142
  - lib/json/ld/version.rb