json-ld 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: