json-ld 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ === 0.0.8
2
+ * RDF.rb 0.3.4 compatibility.
3
+ * Format detection.
4
+ * Use new @list syntax for parsing ordered collections.
5
+ * Separate normalize from canonicalize
6
+
7
+
8
+ === 0.0.7
9
+ * Change MIME Type and extension from application/json, .json to application/ld+json, .jsonld.
10
+ * Also added application/x-ld+json
11
+ * Process a remote @context
12
+ * Updated to current state of spec, including support for aliased keywords
13
+ * Update Writer to output consistent with current spec.
14
+
15
+ === 0.0.6
16
+ * Another order problem (in literals)
17
+
18
+ === 0.0.5
19
+ * Fix @literal, @language, @datatype, and @iri serialization
20
+ * Use InsertOrderPreservingHash for Ruby 1.8
21
+
22
+ === 0.0.4
23
+ * Fixed ruby 1.8 hash-order problem when parsing @context.
24
+ * Add .jsonld file extention and format
25
+ * JSON-LD Writer
26
+ * Use test suite from json.org
27
+ * Use '@type' instead of 'a' and '@subject' instead of '@'
28
+
29
+ === 0.0.3
30
+ * Downgrade RDF.rb requirement from 0.4.0 to 0.3.3.
31
+
32
+ === 0.0.2
33
+ * Functional Reader with reasonable test coverage.
34
+
35
+ === 0.0.1
36
+ * First release of project scaffold.
@@ -22,7 +22,7 @@ Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-ld/0.0.4
22
22
  * {JSON::LD::Reader}
23
23
  * {JSON::LD::Writer}
24
24
 
25
- ##Dependencies
25
+ ## Dependencies
26
26
  * [Ruby](http://ruby-lang.org/) (>= 1.8.7) or (>= 1.8.1 with [Backports][])
27
27
  * [RDF.rb](http://rubygems.org/gems/rdf) (>= 0.3.4)
28
28
  * [JSON](https://rubygems.org/gems/json) (>= 1.5.1)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.8
1
+ 0.1.0
@@ -23,27 +23,103 @@ module JSON
23
23
  require 'json'
24
24
  require 'json/ld/extensions'
25
25
  require 'json/ld/format'
26
- autoload :Normalize, 'json/ld/normalize'
27
- autoload :Reader, 'json/ld/reader'
28
- autoload :VERSION, 'json/ld/version'
29
- autoload :Writer, 'json/ld/writer'
26
+ autoload :API, 'json/ld/api'
27
+ autoload :EvaluationContext, 'json/ld/evaluation_context'
28
+ autoload :Normalize, 'json/ld/normalize'
29
+ autoload :Reader, 'json/ld/reader'
30
+ autoload :VERSION, 'json/ld/version'
31
+ autoload :Writer, 'json/ld/writer'
30
32
 
31
- # Default context
32
- # @see http://json-ld.org/spec/ED/20110507/#the-default-context
33
- DEFAULT_CONTEXT = {
34
- '@coerce' => {
35
- '@iri' => ['@type']
36
- }
33
+ # Initial context
34
+ # @see http://json-ld.org/spec/latest/json-ld-api/#appendix-b
35
+ INITIAL_CONTEXT = {
36
+ RDF.type.to_s => {"@type" => "@id"}
37
37
  }.freeze
38
38
 
39
- # Default type coercion, in property => datatype order
40
- DEFAULT_COERCE = {
41
- '@type' => '@iri',
42
- RDF.first.to_s => false, # Make sure @coerce isn't generated for this
43
- RDF.rest.to_s => '@iri',
44
- }.freeze
39
+ # Regexp matching an NCName.
40
+ NC_REGEXP = Regexp.new(
41
+ %{^
42
+ (?!\\\\u0301) # ́ is a non-spacing acute accent.
43
+ # It is legal within an XML Name, but not as the first character.
44
+ ( [a-zA-Z_]
45
+ | \\\\u[0-9a-fA-F]
46
+ )
47
+ ( [0-9a-zA-Z_\.-]
48
+ | \\\\u([0-9a-fA-F]{4})
49
+ )*
50
+ $},
51
+ Regexp::EXTENDED)
52
+
53
+ # Datatypes that are expressed in a native form and don't expand or compact
54
+ NATIVE_DATATYPES = [RDF::XSD.integer.to_s, RDF::XSD.boolean.to_s, RDF::XSD.double.to_s]
45
55
 
46
56
  def self.debug?; @debug; end
47
57
  def self.debug=(value); @debug = value; end
58
+
59
+ class ProcessingError < Exception
60
+ # The compaction would lead to a loss of information, such as a @language value.
61
+ LOSSY_COMPACTION = 1
62
+
63
+ # The target datatype specified in the coercion rule and the datatype for the typed literal do not match.
64
+ CONFLICTING_DATATYPES = 2
65
+
66
+ attr :code
67
+
68
+ class Lossy < ProcessingError
69
+ def initialize(*args)
70
+ super
71
+ @code = LOSSY_COMPACTION
72
+ end
73
+ end
74
+
75
+ class Conflict < ProcessingError
76
+ def initialize(*args)
77
+ super
78
+ @code = CONFLICTING_DATATYPES
79
+ end
80
+ end
81
+ end
82
+
83
+ class InvalidContext < Exception
84
+ # A general syntax error was detected in the @context. For example, if a @type key maps to anything
85
+ # other than @id or an absolute IRI, this exception would be raised.
86
+ INVALID_SYNTAX = 1
87
+
88
+ # There was a problem encountered loading a remote context.
89
+ LOAD_ERROR = 2
90
+
91
+ attr :code
92
+
93
+ class Syntax < InvalidContext
94
+ def initialize(*args)
95
+ super
96
+ @code = INVALID_SYNTAX
97
+ end
98
+ end
99
+
100
+ class LoadError < InvalidContext
101
+ def initialize(*args)
102
+ super
103
+ @code = LOAD_ERROR
104
+ end
105
+ end
106
+ end
107
+
108
+ class InvalidFrame < Exception
109
+ # A frame must be either an object or an array of objects, if the frame is neither of these types,
110
+ # this exception is thrown.
111
+ INVALID_SYNTAX = 1
112
+
113
+ # A subject IRI was specified in more than one place in the input frame.
114
+ # More than one embed of a given subject IRI is not allowed, and if requested, must result in this exception.
115
+ MULTIPLE_EMBEDS = 2
116
+
117
+ attr :code
118
+
119
+ def intialize(message, code = nil)
120
+ super(message)
121
+ @code = code
122
+ end
123
+ end
48
124
  end
49
125
  end
@@ -0,0 +1,320 @@
1
+ require 'open-uri'
2
+
3
+ module JSON::LD
4
+ ##
5
+ # A JSON-LD processor implementing the JsonLdProcessor interface.
6
+ #
7
+ # This API provides a clean mechanism that enables developers to convert JSON-LD data into a a variety of output formats that
8
+ # are easier to work with in various programming languages. If a JSON-LD API is provided in a programming environment, the
9
+ # entirety of the following API must be implemented.
10
+ #
11
+ # @see http://json-ld.org/spec/latest/json-ld-api/#the-application-programming-interface
12
+ # @author [Gregg Kellogg](http://greggkellogg.net/)
13
+ class API
14
+ attr_accessor :value
15
+ attr_accessor :context
16
+
17
+ ##
18
+ # Initialize the API, reading in any document and setting global options
19
+ #
20
+ # @param [#read, Hash, Array] input
21
+ # @param [IO, Hash, Array] context
22
+ # An external context to use additionally to the context embedded in input when expanding the input.
23
+ # @param [Hash] options
24
+ # @yield [api]
25
+ # @yieldparam [API]
26
+ def initialize(input, context, options = {})
27
+ @options = options
28
+ @value = input.respond_to?(:read) ? JSON.parse(input.read) : input
29
+ @context = EvaluationContext.new(options)
30
+ @context = @context.parse(context) if context
31
+ yield(self) if block_given?
32
+ end
33
+
34
+ ##
35
+ # Expands the given input according to the steps in the Expansion Algorithm. The input must be copied, expanded and returned
36
+ # if there are no errors. If the expansion fails, an appropriate exception must be thrown.
37
+ #
38
+ # @param [#read, Hash, Array] input
39
+ # The JSON-LD object to copy and perform the expansion upon.
40
+ # @param [IO, Hash, Array] context
41
+ # An external context to use additionally to the context embedded in input when expanding the input.
42
+ # @param [Hash{Symbol => Object}] options
43
+ # @raise [InvalidContext]
44
+ # @return [Hash, Array]
45
+ # The expanded JSON-LD document
46
+ # @see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
47
+ def self.expand(input, context = nil, options = {})
48
+ result = nil
49
+ API.new(input, context, options) do |api|
50
+ result = api.expand(api.value, nil, api.context)
51
+ end
52
+ result
53
+ end
54
+
55
+ ##
56
+ # Expand an Array or Object given an active context and performing local context expansion.
57
+ #
58
+ # @param [Array, Hash] input
59
+ # @param [RDF::URI] predicate
60
+ # @param [EvaluationContext] context
61
+ # @return [Array, Hash]
62
+ def expand(input, predicate, context)
63
+ debug("expand") {"input: #{input.class}, predicate: #{predicate.inspect}, context: #{context.inspect}"}
64
+ case input
65
+ when Array
66
+ # 1) If value is an array, process each item in value recursively using this algorithm,
67
+ # passing copies of the active context and active property.
68
+ depth {input.map {|v| expand(v, predicate, context)}}
69
+ when Hash
70
+ # Merge context
71
+ context = context.parse(input['@context']) if input['@context']
72
+
73
+ result = Hash.new
74
+ input.each do |key, value|
75
+ debug("expand") {"#{key}: #{value.inspect}"}
76
+ expanded_key = context.mapping(key) || key
77
+ case expanded_key
78
+ when '@context'
79
+ # Ignore in output
80
+ when '@id', '@type'
81
+ # If the key is @id or @type and the value is a string, expand the value according to IRI Expansion.
82
+ result[expanded_key] = case value
83
+ when String then context.expand_iri(value, :position => :subject, :depth => @depth).to_s
84
+ else depth { expand(value, predicate, context) }
85
+ end
86
+ debug("expand") {" => #{result[expanded_key].inspect}"}
87
+ when '@literal', '@language'
88
+ raise ProcessingError::Lossy, "Value of #{expanded_key} must be a string, was #{value.inspect}" unless value.is_a?(String)
89
+ result[expanded_key] = value
90
+ debug("expand") {" => #{result[expanded_key].inspect}"}
91
+ else
92
+ # 2.2.3) Otherwise, if the key is not a keyword, expand the key according to IRI Expansion rules and set as active property.
93
+ unless key[0,1] == '@'
94
+ predicate = context.expand_iri(key, :position => :predicate, :depth => @depth)
95
+ expanded_key = predicate.to_s
96
+ end
97
+
98
+ # 2.2.4) If the value is an array, and active property is subject to @list expansion,
99
+ # replace the value with a new key-value key where the key is @list and value set to the current value.
100
+ value = {"@list" => value} if value.is_a?(Array) && context.list(predicate)
101
+
102
+ value = case value
103
+ # 2.2.5) If the value is an array, process each item in the array recursively using this algorithm,
104
+ # passing copies of the active context and active property
105
+ # 2.2.6) If the value is an object, process the object recursively using this algorithm,
106
+ # passing copies of the active context and active property.
107
+ when Array, Hash then depth {expand(value, predicate, context)}
108
+ else
109
+ # 2.2.7) Otherwise, expand the value according to the Value Expansion rules, passing active property.
110
+ context.expand_value(predicate, value, :position => :object, :depth => @depth)
111
+ end
112
+ result[expanded_key] = value
113
+ debug("expand") {" => #{value.inspect}"}
114
+ end
115
+ end
116
+ result
117
+ else
118
+ # 2.3) Otherwise, expand the value according to the Value Expansion rules, passing active property.
119
+ context.expand_value(predicate, input, :position => :object, :depth => @depth)
120
+ end
121
+ end
122
+
123
+ ##
124
+ # Compacts the given input according to the steps in the Compaction Algorithm. The input must be copied, compacted and
125
+ # returned if there are no errors. If the compaction fails, an appropirate exception must be thrown.
126
+ #
127
+ # If no context is provided, the input document is compacted using the top-level context of the document
128
+ #
129
+ # @param [IO, Hash, Array] input
130
+ # The JSON-LD object to copy and perform the compaction upon.
131
+ # @param [IO, Hash, Array] context
132
+ # The base context to use when compacting the input.
133
+ # @param [Hash{Symbol => Object}] options
134
+ # @raise [InvalidContext, ProcessingError]
135
+ # @return [Hash]
136
+ # The compacted JSON-LD document
137
+ # @see http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
138
+ def self.compact(input, context = nil, options = {})
139
+ expanded = result = nil
140
+
141
+ API.new(input, nil, options) do |api|
142
+ expanded = api.expand(api.value, nil, api.context)
143
+ end
144
+
145
+ API.new(expanded, context, options) do |api|
146
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
147
+ # This removes any existing context to allow the given context to be cleanly applied.
148
+
149
+ result = api.compact(api.value, nil)
150
+
151
+ # xxx) Add the given context to the output
152
+ result = case result
153
+ when Hash then api.context.serialize.merge(result)
154
+ when Array then api.context.serialize.merge("@id" => result)
155
+ end
156
+ end
157
+ result
158
+ end
159
+
160
+ ##
161
+ # Compact an expanded Array or Hash given an active property and a context.
162
+ #
163
+ # @param [Array, Hash] input
164
+ # @param [RDF::URI] predicate (nil)
165
+ # @param [EvaluationContext] context
166
+ # @return [Array, Hash]
167
+ def compact(input, predicate = nil)
168
+ debug("compact") {"input: #{input.class}, predicate: #{predicate.inspect}"}
169
+ case input
170
+ when Array
171
+ # 1) If value is an array, process each item in value recursively using this algorithm,
172
+ # passing copies of the active context and active property.
173
+ debug("compact") {"Array[#{input.length}]"}
174
+ depth {input.map {|v| compact(v, predicate)}}
175
+ when Hash
176
+ result = Hash.new
177
+ input.each do |key, value|
178
+ debug("compact") {"#{key}: #{value.inspect}"}
179
+ compacted_key = context.alias(key)
180
+ debug("compact") {" => compacted key: #{compacted_key.inspect}"} unless compacted_key == key
181
+
182
+ case key
183
+ when '@id', '@type'
184
+ # If the key is @id or @type
185
+ result[compacted_key] = case value
186
+ when String, RDF::Value
187
+ # If the value is a string, compact the value according to IRI Compaction.
188
+ context.compact_iri(value, :position => :subject, :depth => @depth).to_s
189
+ when Hash
190
+ # Otherwise, if value is an object containing only the @id key, the compacted value
191
+ # if the result of performing IRI Compaction on that value.
192
+ if value.keys == ["@id"]
193
+ context.compact_iri(value["@id"], :position => :subject, :depth => @depth).to_s
194
+ else
195
+ depth { compact(value, predicate) }
196
+ end
197
+ else
198
+ # Otherwise, the compacted value is the result of performing this algorithm on the value
199
+ # with the current active property.
200
+ depth { compact(value, predicate) }
201
+ end
202
+ debug("compact") {" => compacted value: #{result[compacted_key].inspect}"}
203
+ else
204
+ # Otherwise, if the key is not a keyword, set as active property and compact according to IRI Compaction.
205
+ unless key[0,1] == '@'
206
+ predicate = RDF::URI(key)
207
+ compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
208
+ debug("compact") {" => compacted key: #{compacted_key.inspect}"}
209
+ end
210
+
211
+ # If the value is an object
212
+ compacted_value = if value.is_a?(Hash)
213
+ if value.keys == ['@id'] || value['@literal']
214
+ # If the value contains only an @id key or the value contains a @literal key, the compacted value
215
+ # is the result of performing Value Compaction on the value.
216
+ debug("compact") {"keys: #{value.keys.inspect}"}
217
+ context.compact_value(predicate, value, :depth => @depth)
218
+ elsif value.keys == ['@list'] && context.list(predicate)
219
+ # Otherwise, if the value contains only a @list key, and the active property is subject to list coercion,
220
+ # the compacted value is the result of performing this algorithm on that value.
221
+ debug("compact") {"list"}
222
+ depth {compact(value['@list'], predicate)}
223
+ else
224
+ # Otherwise, the compacted value is the result of performing this algorithm on the value
225
+ debug("compact") {"object"}
226
+ depth {compact(value, predicate)}
227
+ end
228
+ elsif value.is_a?(Array)
229
+ # Otherwise, if the value is an array, the compacted value is the result of performing this algorithm on the value.
230
+ debug("compact") {"array"}
231
+ depth {compact(value, predicate)}
232
+ else
233
+ # Otherwise, the value is already compacted.
234
+ debug("compact") {"value"}
235
+ value
236
+ end
237
+ debug("compact") {" => compacted value: #{compacted_value.inspect}"}
238
+ result[compacted_key || key] = compacted_value
239
+ end
240
+ end
241
+ result
242
+ else
243
+ # For other types, the compacted value is the input value
244
+ debug("compact") {input.class.to_s}
245
+ input
246
+ end
247
+ end
248
+
249
+ ##
250
+ # Frames the given input using the frame according to the steps in the Framing Algorithm. The input is used to build the
251
+ # framed output and is returned if there are no errors. If there are no matches for the frame, null must be returned.
252
+ # Exceptions must be thrown if there are errors.
253
+ #
254
+ # @param [IO, Hash, Array] input
255
+ # The JSON-LD object to copy and perform the framing on.
256
+ # @param [IO, Hash, Array] frame
257
+ # The frame to use when re-arranging the data.
258
+ # @param [Hash{Symbol => Object}] options
259
+ # @raise [InvalidFrame]
260
+ # @return [Hash]
261
+ # The framed JSON-LD document
262
+ def self.frame(input, frame, options = {})
263
+ end
264
+
265
+ ##
266
+ # Normalizes the given input according to the steps in the Normalization Algorithm. The input must be copied, normalized and
267
+ # returned if there are no errors. If the compaction fails, null must be returned.
268
+ #
269
+ # @param [IO, Hash, Array] input
270
+ # The JSON-LD object to copy and perform the normalization upon.
271
+ # @param [IO, Hash, Array] context
272
+ # An external context to use additionally to the context embedded in input when expanding the input.
273
+ # @param [Hash{Symbol => Object}] options
274
+ # @raise [InvalidContext]
275
+ # @return [Hash]
276
+ # The normalized JSON-LD document
277
+ def self.normalize(input, object, context = nil, options = {})
278
+ end
279
+
280
+ ##
281
+ # Processes the input according to the RDF Conversion Algorithm, calling the provided tripleCallback for each triple generated.
282
+ #
283
+ # @param [IO, Hash, Array] input
284
+ # The JSON-LD object to process when outputting triples.
285
+ # @param [IO, Hash, Array] context
286
+ # An external context to use additionally to the context embedded in input when expanding the input.
287
+ # @param [Hash{Symbol => Object}] options
288
+ # @raise [InvalidContext]
289
+ # @yield statement
290
+ # @yieldparam [RDF::Statement] statement
291
+ # @return [Hash]
292
+ # The normalized JSON-LD document
293
+ def self.triples(input, object, context = nil, options = {})
294
+ end
295
+
296
+ private
297
+ # Add debug event to debug array, if specified
298
+ #
299
+ # @param [String] message
300
+ # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
301
+ def debug(*args)
302
+ return unless ::JSON::LD.debug? || @options[:debug]
303
+ list = args
304
+ list << yield if block_given?
305
+ message = " " * (@depth || 0) * 2 + (list.empty? ? "" : list.join(": "))
306
+ puts message if JSON::LD::debug?
307
+ @options[:debug] << message if @options[:debug].is_a?(Array)
308
+ end
309
+
310
+ # Increase depth around a method invocation
311
+ def depth(options = {})
312
+ old_depth = @depth || 0
313
+ @depth = (options[:depth] || old_depth) + 1
314
+ ret = yield
315
+ @depth = old_depth
316
+ ret
317
+ end
318
+ end
319
+ end
320
+