json-ld 0.0.1 → 0.0.2

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.1
1
+ 0.0.2
@@ -1,5 +1,5 @@
1
1
  $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..')))
2
- require 'rdf'
2
+ require 'rdf' # @see http://rubygems.org/gems/rdf
3
3
 
4
4
  module JSON
5
5
  ##
@@ -20,6 +20,7 @@ module JSON
20
20
  #
21
21
  # @author [Gregg Kellogg](http://greggkellogg.net/)
22
22
  module LD
23
+ require 'json'
23
24
  require 'json/ld/format'
24
25
  autoload :Reader, 'json/ld/reader'
25
26
  autoload :VERSION, 'json/ld/version'
@@ -1,26 +1,31 @@
1
- module RDF::N3
1
+ module JSON::LD
2
2
  ##
3
3
  # RDFa format specification.
4
4
  #
5
5
  # @example Obtaining an Notation3 format class
6
- # RDF::Format.for(:jld) #=> JSON::LD::Format
7
- # RDF::Format.for("etc/foaf.jld")
8
- # RDF::Format.for(:file_name => "etc/foaf.jld")
9
- # RDF::Format.for(:file_extension => "jld")
6
+ # RDF::Format.for(:json) #=> JSON::LD::Format
7
+ # RDF::Format.for(:ld) #=> JSON::LD::Format
8
+ # RDF::Format.for("etc/foaf.json")
9
+ # RDF::Format.for("etc/foaf.ld")
10
+ # RDF::Format.for(:file_name => "etc/foaf.json")
11
+ # RDF::Format.for(:file_name => "etc/foaf.ld")
12
+ # RDF::Format.for(:file_extension => "json")
13
+ # RDF::Format.for(:file_extension => "ld")
10
14
  # RDF::Format.for(:content_type => "application/json")
11
15
  #
12
16
  # @example Obtaining serialization format MIME types
13
17
  # RDF::Format.content_types #=> {"application/json" => [JSON::LD::Format]}
14
18
  #
15
19
  # @example Obtaining serialization format file extension mappings
16
- # RDF::Format.file_extensions #=> {:ttl => "application/json"}
20
+ # RDF::Format.file_extensions #=> {:json => "application/json"}
17
21
  #
18
22
  # @see http://www.w3.org/TR/rdf-testcases/#ntriples
19
23
  class Format < RDF::Format
20
- content_type 'application/json', :extension => :jld
24
+ content_type 'application/json', :extension => :json
25
+ content_type 'application/json', :extension => :ld
21
26
  content_encoding 'utf-8'
22
27
 
23
- reader { RDF::N3::Reader }
24
- writer { RDF::N3::Writer }
28
+ reader { JSON::LD::Reader }
29
+ writer { JSON::LD::Writer }
25
30
  end
26
31
  end
@@ -6,6 +6,388 @@ module JSON::LD
6
6
  # @author [Gregg Kellogg](http://greggkellogg.net/)
7
7
  class Reader < RDF::Reader
8
8
  format Format
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
+ ##
38
+ # The graph constructed when parsing.
39
+ #
40
+ # @return [RDF::Graph]
41
+ attr_reader :graph
42
+
43
+ ##
44
+ # Context
45
+ #
46
+ # The `@context` keyword is used to change how the JSON-LD processor evaluates key- value pairs. In this
47
+ # case, it was used to map one string (`'myvocab'`) to another string, which is interpreted as a IRI. In the
48
+ # example above, the `myvocab` string is replaced with "http://example.org/myvocab#" when it is detected. In
49
+ # the example above, `"myvocab:personality"` would expand to "http://example.org/myvocab#personality".
50
+ #
51
+ # This mechanism is a short-hand for RDF, called a `CURIE`, and provides developers an unambiguous way to
52
+ # map any JSON value to RDF.
53
+ #
54
+ # @private
55
+ class EvaluationContext # :nodoc:
56
+ # The base.
57
+ #
58
+ # The `@base` string is a special keyword that states that any relative IRI MUST be appended to the string
59
+ # specified by `@base`.
60
+ #
61
+ # @attr [RDF::URI]
62
+ attr :base, true
63
+
64
+ # A list of current, in-scope URI mappings.
65
+ #
66
+ # @attr [Hash{Symbol => String}]
67
+ attr :mappings, true
68
+
69
+ # The default vocabulary
70
+ #
71
+ # A value to use as the prefix URI when a term is used.
72
+ # This specification does not define an initial setting for the default vocabulary.
73
+ # Host Languages may define an initial setting.
74
+ #
75
+ # @attr [String]
76
+ attr :vocab, true
77
+
78
+ # Type coersion
79
+ #
80
+ # The @coerce keyword is used to specify type coersion rules for the data. For each key in the map, the
81
+ # key is the type to be coerced to and the value is the vocabulary term to be coerced. Type coersion for
82
+ # the key `xsd:anyURI` asserts that all vocabulary terms listed should undergo coercion to an IRI,
83
+ # including `@base` processing for relative IRIs and CURIE processing for compact URI Expressions like
84
+ # `foaf:homepage`.
85
+ #
86
+ # As the value may be an array, this is maintained as a reverse mapping of `property` => `type`.
87
+ #
88
+ # @attr [Hash{RDF::URI => RDF::URI}]
89
+ attr :coerce, true
90
+
91
+ ##
92
+ # Create new evaluation context
93
+ # @yield [ec]
94
+ # @yieldparam [EvaluationContext]
95
+ # @return [EvaluationContext]
96
+ def initialize
97
+ @base = nil
98
+ @mappings = {}
99
+ @vocab = nil
100
+ @coerce = {}
101
+ yield(self) if block_given?
102
+ end
103
+
104
+ def inspect
105
+ v = %w([EvaluationContext) + %w(base vocab).map {|a| "#{a}='#{self.send(a).inspect}'"}
106
+ v << "mappings[#{mappings.keys.length}]"
107
+ v << "coerce[#{coerce.keys.length}]"
108
+ v.join(", ") + "]"
109
+ end
110
+ end
111
+
112
+ ##
113
+ # Initializes the RDF/JSON reader instance.
114
+ #
115
+ # @param [IO, File, String] input
116
+ # @param [Hash{Symbol => Object}] options
117
+ # any additional options (see `RDF::Reader#initialize`)
118
+ # @yield [reader] `self`
119
+ # @yieldparam [RDF::Reader] reader
120
+ # @yieldreturn [void] ignored
121
+ def initialize(input = $stdin, options = {}, &block)
122
+ super do
123
+ @base_uri = uri(options[:base_uri]) if options[:base_uri]
124
+ @doc = ::JSON.load(input)
125
+
126
+ if block_given?
127
+ case block.arity
128
+ when 0 then instance_eval(&block)
129
+ else block.call(self)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ ##
136
+ # @private
137
+ # @see RDF::Reader#each_statement
138
+ def each_statement(&block)
139
+ @callback = block
140
+
141
+ # initialize the evaluation context with the appropriate base
142
+ ec = EvaluationContext.new do |ec|
143
+ ec.base = @base_uri if @base_uri
144
+ parse_context(ec, DEFAULT_CONTEXT)
145
+ end
146
+
147
+ traverse("", @doc, nil, nil, ec)
148
+ end
149
+
150
+ ##
151
+ # @private
152
+ # @see RDF::Reader#each_triple
153
+ def each_triple(&block)
154
+ each_statement do |statement|
155
+ block.call(*statement.to_triple)
156
+ end
157
+ end
158
+
159
+ private
160
+ ##
161
+ #
162
+ # @param [String] path
163
+ # location within JSON hash
164
+ # @param [Hash, Array, String] element
165
+ # The current JSON element being processed
166
+ # @param [RDF::URI] subject
167
+ # Inherited subject
168
+ # @param [RDF::URI] property
169
+ # Inherited property
170
+ # @param [EvaluationContext] ec
171
+ # The active context
172
+ def traverse(path, element, subject, property, ec)
173
+ add_debug(path, "traverse: s=#{subject.inspect}, p=#{property.inspect}, e=#{ec.inspect}")
174
+ object = nil
175
+
176
+ case element
177
+ when Hash
178
+ # 2) ... For each key-value
179
+ # pair in the associative array, using the newly created processor state do the
180
+ # following:
181
+
182
+ # 2.1) If a @context keyword is found, the processor merges each key-value pair in
183
+ # the local context into the active context ...
184
+ if element["@context"]
185
+ # Merge context
186
+ ec = parse_context(ec.dup, element["@context"])
187
+ prefixes.merge!(ec.mappings) # Update parsed prefixes
188
+ end
189
+
190
+ # Other shortcuts to allow use of this method for terminal associative arrays
191
+ if element["@iri"].is_a?(String)
192
+ # Return the IRI found from the value
193
+ object = expand_term(element["@iri"], ec.base, ec)
194
+ add_triple(path, subject, property, object) if subject && property
195
+ elsif element["@literal"]
196
+ 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)
200
+ add_triple(path, subject, property, object) if subject && property
201
+ end
202
+
203
+ # 2.2) ... Otherwise, if the local context is known perform the following steps:
204
+ # 2.2.1) If a @ key is found, the processor sets the active subject to the
205
+ # 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
216
+ else
217
+ # 2.2.7) If the end of the associative array is detected, and a active subject
218
+ # was not discovered, then:
219
+ # 2.2.7.1) Generate a blank node identifier and set it as the active subject.
220
+ subject = RDF::Node.new
221
+ end
222
+
223
+ element.each do |key, value|
224
+ # 2.2.3) If a key that is not @context, @, or a, set the active property by
225
+ # performing Property Processing on the key.
226
+ property = case key
227
+ when /^@/
228
+ nil
229
+ when 'a'
230
+ RDF.type
231
+ else
232
+ expand_term(key, ec.vocab, ec)
233
+ end
234
+
235
+ traverse("#{path}[#{key}]", value, subject, property, ec) if property
236
+ end
237
+ when Array
238
+ # 3) If a regular array is detected, process each value in the array by doing the following:
239
+ element.each_with_index do |v, i|
240
+ case v
241
+ when Hash, String
242
+ traverse("#{path}[#{i}]", v, subject, property, ec)
243
+ when Array
244
+ # 3.3) If the value is a regular array, should we support RDF List/Sequence Processing?
245
+ last = v.pop
246
+ first_bnode = last ? RDF::Node.new : RDF.nil
247
+ add_triple("#{path}[#{i}][]", subject, property, first_bnode)
248
+
249
+ v.each do |list_item|
250
+ traverse("#{path}[#{i}][]", list_item, first_bnode, RDF.first, ec)
251
+ rest_bnode = RDF::Node.new
252
+ add_triple("#{path}[#{i}][]", first_bnode, RDF.rest, rest_bnode)
253
+ first_bnode = rest_bnode
254
+ end
255
+ if last
256
+ traverse("#{path}[#{i}][]", last, first_bnode, RDF.first, ec)
257
+ add_triple("#{path}[#{i}][]", first_bnode, RDF.rest, RDF.nil)
258
+ end
259
+ end
260
+ end
261
+ when String
262
+ # Perform coersion of the value, or generate a literal
263
+ add_debug(path, "traverse(#{element}): coerce?(#{property.inspect}) == #{ec.coerce[property].inspect}, ec=#{ec.coerce.inspect}")
264
+ object = if ec.coerce[property] == RDF::XSD.anyURI
265
+ expand_term(element, ec.base, ec)
266
+ elsif ec.coerce[property]
267
+ RDF::Literal.new(element, :datatype => ec.coerce[property])
268
+ else
269
+ RDF::Literal.new(element)
270
+ end
271
+ add_triple(path, subject, property, object) if subject && property
272
+ when Float
273
+ object = RDF::Literal::Double.new(element)
274
+ add_debug(path, "traverse(#{element}): native: #{object.inspect}")
275
+ add_triple(path, subject, property, object) if subject && property
276
+ when Fixnum
277
+ object = RDF::Literal.new(element)
278
+ add_debug(path, "traverse(#{element}): native: #{object.inspect}")
279
+ add_triple(path, subject, property, object) if subject && property
280
+ when TrueClass, FalseClass
281
+ object = RDF::Literal::Boolean.new(element)
282
+ add_debug(path, "traverse(#{element}): native: #{object.inspect}")
283
+ add_triple(path, subject, property, object) if subject && property
284
+ else
285
+ raise RDF::ReaderError, "Traverse to unknown element: #{element.inspect} of type #{element.class}"
286
+ end
287
+ end
288
+
289
+ ##
290
+ # add a statement, object can be literal or URI or bnode
291
+ #
292
+ # @param [String] path
293
+ # @param [URI, BNode] subject the subject of the statement
294
+ # @param [URI] predicate the predicate of the statement
295
+ # @param [URI, BNode, Literal] object the object of the statement
296
+ # @return [Statement] Added statement
297
+ # @raise [ReaderError] Checks parameter types and raises if they are incorrect if parsing mode is _validate_.
298
+ def add_triple(path, subject, predicate, object)
299
+ statement = RDF::Statement.new(subject, predicate, object)
300
+ add_debug(path, "statement: #{statement.to_ntriples}")
301
+ @callback.call(statement)
302
+ end
303
+
304
+ ##
305
+ # Add debug event to debug array, if specified
306
+ #
307
+ # @param [XML Node, any] node:: XML Node or string for showing context
308
+ # @param [String] message::
309
+ def add_debug(node, message)
310
+ puts "#{node}: #{message}" if JSON::LD::debug?
311
+ @options[:debug] << "#{node}: #{message}" if @options[:debug].is_a?(Array)
312
+ end
313
+
314
+ ##
315
+ # Parse a JSON context, into a new EvaluationContext
316
+ # @param [Hash{String => String,Hash}] context
317
+ # JSON representation of @context
318
+ # @return [EvaluationContext]
319
+ # @raise [RDF::ReaderError] on a syntax error, or a reference to a term which is not defined.
320
+ def parse_context(ec, context)
321
+ context.each do |key, value|
322
+ #add_debug("parse_context(#{key})", value.inspect)
323
+ 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
339
+ else
340
+ # Spec confusion: The text indicates to merge each key-value pair into the active context. Is any
341
+ # processing performed on the values. For instance, could a value be a CURIE, or {"@iri": <value>}?
342
+ # Examples indicate that there is no such processing, and each value should be an absolute IRI. The
343
+ # wording makes this unclear.
344
+ ec.mappings[key.to_sym] = value
345
+ end
346
+ end
347
+
348
+ ec
349
+ end
350
+
351
+ ##
352
+ # Expand a term using the specified context
353
+ #
354
+ # @param [String] term
355
+ # @param [String] base Base to apply to URIs
356
+ # @param [EvaluationContext] ec
357
+ #
358
+ # @return [RDF::URI]
359
+ # @raise [RDF::ReaderError] if the term cannot be expanded
360
+ # @see http://json-ld.org/spec/ED/20110507/#markup-of-rdf-concepts
361
+ def expand_term(term, base, ec)
362
+ #add_debug("expand_term", "term=#{term.inspect}, base=#{base.inspect}, ec=#{ec.inspect}")
363
+ prefix, suffix = term.split(":", 2)
364
+ prefix = prefix.to_sym if prefix
365
+ return if prefix == '_'
366
+ if prefix == :_
367
+ bnode(suffix)
368
+ elsif ec.mappings.has_key?(prefix)
369
+ uri(ec.mappings[prefix] + suffix.to_s)
370
+ elsif base
371
+ base.respond_to?(:join) ? base.join(term) : uri(base + term)
372
+ else
373
+ uri(term)
374
+ end
375
+ end
376
+
377
+ def uri(value, append = nil)
378
+ value = RDF::URI.new(value)
379
+ value = value.join(append) if append
380
+ value.validate! if validate?
381
+ value.canonicalize! if canonicalize?
382
+ value = RDF::URI.intern(value) if intern?
383
+ value
384
+ end
385
+
386
+ # Keep track of allocated BNodes
387
+ def bnode(value = nil)
388
+ @bnode_cache ||= {}
389
+ @bnode_cache[value.to_s] ||= RDF::Node.new(value)
390
+ end
9
391
  end
10
392
  end
11
393
 
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.1
5
+ version: 0.0.2
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-07 00:00:00 -07:00
13
+ date: 2011-05-09 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -87,7 +87,7 @@ files:
87
87
  - lib/json/ld/writer.rb
88
88
  - lib/json/ld.rb
89
89
  has_rdoc: false
90
- homepage: http://rdf.rubyforge.org/json-ld
90
+ homepage: http://github.com/gkellogg/json-ld
91
91
  licenses:
92
92
  - Public Domain
93
93
  post_install_message: