json-ld 0.0.8 → 0.1.0

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.
@@ -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
+