json-ld 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,18 @@
1
+ === 0.1.1
2
+ * Changed @literal to @value.
3
+ * Change expanded double format to %1.6e
4
+ * Only recognize application/ld+json and :jsonld.
5
+
6
+ === 0.1.0
7
+ * New @context processing rules.
8
+ * @iri and @subject changed to @id.
9
+ * @datatype changed to @type.
10
+ * @coerce keys can be CURIEs or IRIs (not spec'd).
11
+ * @language in @context.
12
+ * Implemented JSON::LD::API for .compact and .expand.
13
+ * Support relative IRI expansion based on document location.
14
+ * Make sure that keyword aliases are fully considered on both input and output and used when compacting.
15
+
1
16
  === 0.0.8
2
17
  * RDF.rb 0.3.4 compatibility.
3
18
  * Format detection.
@@ -4,7 +4,8 @@
4
4
 
5
5
  ## Features
6
6
 
7
- JSON::LD parses and serializes [JSON-LD][] into statements or triples.
7
+ JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements
8
+ JSON::LD expansion, compaction and framing API interfaces.
8
9
 
9
10
  Install with `gem install json-ld`
10
11
 
@@ -13,13 +14,208 @@ Install with `gem install json-ld`
13
14
  require 'rubygems'
14
15
  require 'json/ld'
15
16
 
17
+ ### Expand a Document
18
+
19
+ input = {
20
+ "@context": {
21
+ "name": "http://xmlns.com/foaf/0.1/name",
22
+ "homepage": "http://xmlns.com/foaf/0.1/homepage",
23
+ "avatar": "http://xmlns.com/foaf/0.1/avatar"
24
+ },
25
+ "name": "Manu Sporny",
26
+ "homepage": "http://manu.sporny.org/",
27
+ "avatar": "http://twitter.com/account/profile_image/manusporny"
28
+ }
29
+ JSON::LD::API.expand(input) =>
30
+
31
+ [{
32
+ "http://xmlns.com/foaf/0.1/name": ["Manu Sporny"],
33
+ "http://xmlns.com/foaf/0.1/homepage": ["http://manu.sporny.org/"],
34
+ "http://xmlns.com/foaf/0.1/avatar": ["http://twitter.com/account/profile_image/manusporny"]
35
+ }]
36
+
37
+ ### Compact a Document
38
+
39
+ input = [{
40
+ "http://xmlns.com/foaf/0.1/name": ["Manu Sporny"],
41
+ "http://xmlns.com/foaf/0.1/homepage": ["http://manu.sporny.org/"],
42
+ "http://xmlns.com/foaf/0.1/avatar": ["http://twitter.com/account/profile_image/manusporny"]
43
+ }]
44
+
45
+ context = {
46
+ "@context": {
47
+ "name": "http://xmlns.com/foaf/0.1/name",
48
+ "homepage": "http://xmlns.com/foaf/0.1/homepage",
49
+ "avatar": "http://xmlns.com/foaf/0.1/avatar"
50
+ }
51
+ }
52
+
53
+ JSON::LD::API.compact(input, context) =>
54
+ {
55
+ "@context": {
56
+ "avatar": "http://xmlns.com/foaf/0.1/avatar",
57
+ "homepage": "http://xmlns.com/foaf/0.1/homepage",
58
+ "name": "http://xmlns.com/foaf/0.1/name"
59
+ },
60
+ "avatar": "http://twitter.com/account/profile_image/manusporny",
61
+ "homepage": "http://manu.sporny.org/",
62
+ "name": "Manu Sporny"
63
+ }
64
+
65
+ ### Frame a Document
66
+
67
+ input = {
68
+ "@context": {
69
+ "Book": "http://example.org/vocab#Book",
70
+ "Chapter": "http://example.org/vocab#Chapter",
71
+ "contains": {"@id": "http://example.org/vocab#contains", "@type": "@id"},
72
+ "creator": "http://purl.org/dc/terms/creator",
73
+ "description": "http://purl.org/dc/terms/description",
74
+ "Library": "http://example.org/vocab#Library",
75
+ "title": "http://purl.org/dc/terms/title"
76
+ },
77
+ "@graph":
78
+ [{
79
+ "@id": "http://example.com/library",
80
+ "@type": "Library",
81
+ "contains": "http://example.org/library/the-republic"
82
+ },
83
+ {
84
+ "@id": "http://example.org/library/the-republic",
85
+ "@type": "Book",
86
+ "creator": "Plato",
87
+ "title": "The Republic",
88
+ "contains": "http://example.org/library/the-republic#introduction"
89
+ },
90
+ {
91
+ "@id": "http://example.org/library/the-republic#introduction",
92
+ "@type": "Chapter",
93
+ "description": "An introductory chapter on The Republic.",
94
+ "title": "The Introduction"
95
+ }]
96
+ }
97
+
98
+ frame = {
99
+ "@context": {
100
+ "Book": "http://example.org/vocab#Book",
101
+ "Chapter": "http://example.org/vocab#Chapter",
102
+ "contains": "http://example.org/vocab#contains",
103
+ "creator": "http://purl.org/dc/terms/creator",
104
+ "description": "http://purl.org/dc/terms/description",
105
+ "Library": "http://example.org/vocab#Library",
106
+ "title": "http://purl.org/dc/terms/title"
107
+ },
108
+ "@type": "Library",
109
+ "contains": {
110
+ "@type": "Book",
111
+ "contains": {
112
+ "@type": "Chapter"
113
+ }
114
+ }
115
+ }
116
+ JSON::LD.frame(input, frame) =>
117
+ {
118
+ "@context": {
119
+ "Book": "http://example.org/vocab#Book",
120
+ "Chapter": "http://example.org/vocab#Chapter",
121
+ "contains": "http://example.org/vocab#contains",
122
+ "creator": "http://purl.org/dc/terms/creator",
123
+ "description": "http://purl.org/dc/terms/description",
124
+ "Library": "http://example.org/vocab#Library",
125
+ "title": "http://purl.org/dc/terms/title"
126
+ },
127
+ "@graph": [
128
+ {
129
+ "@id": "http://example.com/library",
130
+ "@type": "Library",
131
+ "contains": {
132
+ "@id": "http://example.org/library/the-republic",
133
+ "@type": "Book",
134
+ "contains": {
135
+ "@id": "http://example.org/library/the-republic#introduction",
136
+ "@type": "Chapter",
137
+ "description": "An introductory chapter on The Republic.",
138
+ "title": "The Introduction"
139
+ },
140
+ "creator": "Plato",
141
+ "title": "The Republic"
142
+ }
143
+ }
144
+ ]
145
+ }
146
+
147
+ ### Turn JSON-LD into RDF (Turtle)
148
+
149
+ input = {
150
+ "@context": {
151
+ "": "http://manu.sporny.org/",
152
+ "foaf": "http://xmlns.com/foaf/0.1/"
153
+ },
154
+ "@id": "http://example.org/people#joebob",
155
+ "@type": "foaf:Person",
156
+ "foaf:name": "Joe Bob",
157
+ "foaf:nick": { "@list": [ "joe", "bob", "jaybe" ] }
158
+ }
159
+
160
+ JSON::LD::API.toRDF(input) =>
161
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
162
+
163
+ <http://example.org/people#joebob> a foaf:Person;
164
+ foaf:name "Joe Bob";
165
+ foaf:nick ("joe" "bob" "jaybe") .
166
+
167
+ ### Turn RDF into JSON-LD
168
+
169
+ input =
170
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
171
+
172
+ <http://manu.sporny.org/#me> a foaf:Person;
173
+ foaf:knows [ a foaf:Person;
174
+ foaf:name "Gregg Kellogg"];
175
+ foaf:name "Manu Sporny" .
176
+
177
+ context =
178
+ {
179
+ "@context": {
180
+ "": "http://manu.sporny.org/",
181
+ "foaf": "http://xmlns.com/foaf/0.1/"
182
+ }
183
+ }
184
+
185
+ JSON::LD::fromRDF(input, context) =>
186
+ {
187
+ "@context": {
188
+ "": "http://manu.sporny.org/",
189
+ "foaf": "http://xmlns.com/foaf/0.1/"
190
+ },
191
+ "@id": ":#me",
192
+ "@type": "foaf:Person",
193
+ "foaf:name": "Manu Sporny",
194
+ "foaf:knows": {
195
+ "@type": "foaf:Person",
196
+ "foaf:name": "Gregg Kellogg"
197
+ }
198
+ }
199
+
200
+ ## RDF Reader and Writer
201
+ {JSON::LD} also acts as a normal RDF reader and writer, using the standard RDF.rb reader/writer interfaces:
202
+
203
+ graph = RDF::Graph.load("etc/doap.jsonld", :format => :jsonld)
204
+ graph.dump(:jsonld, :standard_prefixes => true)
205
+
16
206
  ## Documentation
17
- Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-ld/0.0.4/file/README)
207
+ Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-ld/file/README.markdown)
18
208
 
19
209
  ### Principle Classes
20
210
  * {JSON::LD}
211
+ * {JSON::LD::API}
212
+ * {JSON::LD::Compact}
213
+ * {JSON::LD::EvaluationContext}
21
214
  * {JSON::LD::Format}
215
+ * {JSON::LD::Frame}
216
+ * {JSON::LD::FromTriples}
22
217
  * {JSON::LD::Reader}
218
+ * {JSON::LD::Triples}
23
219
  * {JSON::LD::Writer}
24
220
 
25
221
  ## Dependencies
@@ -70,4 +266,4 @@ see <http://unlicense.org/> or the accompanying {file:UNLICENSE} file.
70
266
  [PDD]: http://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html
71
267
  [RDF.rb]: http://rdf.rubyforge.org/
72
268
  [Backports]: http://rubygems.org/gems/backports
73
- [JSON-LD]: http://json-ld.org/spec/ED/20110507/
269
+ [JSON-LD]: http://json-ld.org/spec/latest/
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.2
@@ -1,5 +1,6 @@
1
1
  $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..')))
2
2
  require 'rdf' # @see http://rubygems.org/gems/rdf
3
+ require 'backports' if RUBY_VERSION < "1.9"
3
4
 
4
5
  module JSON
5
6
  ##
@@ -23,6 +24,7 @@ module JSON
23
24
  require 'json'
24
25
  require 'json/ld/extensions'
25
26
  require 'json/ld/format'
27
+ require 'json/ld/utils'
26
28
  autoload :API, 'json/ld/api'
27
29
  autoload :EvaluationContext, 'json/ld/evaluation_context'
28
30
  autoload :Normalize, 'json/ld/normalize'
@@ -36,6 +38,17 @@ module JSON
36
38
  RDF.type.to_s => {"@type" => "@id"}
37
39
  }.freeze
38
40
 
41
+ KEYWORDS = %w(
42
+ @container
43
+ @context
44
+ @id
45
+ @language
46
+ @list
47
+ @set
48
+ @type
49
+ @value
50
+ ).freeze
51
+
39
52
  # Regexp matching an NCName.
40
53
  NC_REGEXP = Regexp.new(
41
54
  %{^
@@ -53,6 +66,14 @@ module JSON
53
66
  # Datatypes that are expressed in a native form and don't expand or compact
54
67
  NATIVE_DATATYPES = [RDF::XSD.integer.to_s, RDF::XSD.boolean.to_s, RDF::XSD.double.to_s]
55
68
 
69
+ JSON_STATE = JSON::State.new(
70
+ :indent => " ",
71
+ :space => " ",
72
+ :space_before => "",
73
+ :object_nl => "\n",
74
+ :array_nl => "\n"
75
+ )
76
+
56
77
  def self.debug?; @debug; end
57
78
  def self.debug=(value); @debug = value; end
58
79
 
@@ -63,6 +84,9 @@ module JSON
63
84
  # The target datatype specified in the coercion rule and the datatype for the typed literal do not match.
64
85
  CONFLICTING_DATATYPES = 2
65
86
 
87
+ # A list containing another list was detected.
88
+ LIST_OF_LISTS_DETECTED = 3
89
+
66
90
  attr :code
67
91
 
68
92
  class Lossy < ProcessingError
@@ -78,6 +102,13 @@ module JSON
78
102
  @code = CONFLICTING_DATATYPES
79
103
  end
80
104
  end
105
+
106
+ class ListOfLists < ProcessingError
107
+ def initialize(*args)
108
+ super
109
+ @code = LIST_OF_LISTS_DETECTED
110
+ end
111
+ end
81
112
  end
82
113
 
83
114
  class InvalidContext < Exception
@@ -115,10 +146,19 @@ module JSON
115
146
  MULTIPLE_EMBEDS = 2
116
147
 
117
148
  attr :code
118
-
119
- def intialize(message, code = nil)
120
- super(message)
121
- @code = code
149
+
150
+ class Syntax < InvalidFrame
151
+ def initialize(*args)
152
+ super
153
+ @code = INVALID_SYNTAX
154
+ end
155
+ end
156
+
157
+ class MultipleEmbeds < InvalidFrame
158
+ def initialize(*args)
159
+ super
160
+ @code = MULTIPLE_EMBEDS
161
+ end
122
162
  end
123
163
  end
124
164
  end
@@ -1,4 +1,9 @@
1
1
  require 'open-uri'
2
+ require 'json/ld/expand'
3
+ require 'json/ld/compact'
4
+ require 'json/ld/frame'
5
+ require 'json/ld/to_rdf'
6
+ require 'json/ld/from_rdf'
2
7
 
3
8
  module JSON::LD
4
9
  ##
@@ -11,113 +16,84 @@ module JSON::LD
11
16
  # @see http://json-ld.org/spec/latest/json-ld-api/#the-application-programming-interface
12
17
  # @author [Gregg Kellogg](http://greggkellogg.net/)
13
18
  class API
19
+ include Expand
20
+ include Compact
21
+ include Triples
22
+ include FromTriples
23
+ include Frame
24
+
14
25
  attr_accessor :value
15
26
  attr_accessor :context
16
27
 
17
28
  ##
18
29
  # Initialize the API, reading in any document and setting global options
19
30
  #
20
- # @param [#read, Hash, Array] input
21
- # @param [IO, Hash, Array] context
31
+ # @param [String, #read, Hash, Array] input
32
+ # @param [String, #read,, Hash, Array] context
22
33
  # An external context to use additionally to the context embedded in input when expanding the input.
23
34
  # @param [Hash] options
24
35
  # @yield [api]
25
36
  # @yieldparam [API]
26
- def initialize(input, context, options = {})
37
+ def initialize(input, context, options = {}, &block)
27
38
  @options = options
28
- @value = input.respond_to?(:read) ? JSON.parse(input.read) : input
39
+ @value = case input
40
+ when Array, Hash then input.dup
41
+ when IO, StringIO then JSON.parse(input.read)
42
+ when String
43
+ content = nil
44
+ RDF::Util::File.open_file(input) {|f| content = JSON.parse(f.read)}
45
+ content
46
+ end
29
47
  @context = EvaluationContext.new(options)
30
48
  @context = @context.parse(context) if context
31
- yield(self) if block_given?
49
+
50
+ if block_given?
51
+ case block.arity
52
+ when 0, -1 then instance_eval(&block)
53
+ else block.call(self)
54
+ end
55
+ end
32
56
  end
33
57
 
34
58
  ##
35
59
  # Expands the given input according to the steps in the Expansion Algorithm. The input must be copied, expanded and returned
36
60
  # if there are no errors. If the expansion fails, an appropriate exception must be thrown.
37
61
  #
38
- # @param [#read, Hash, Array] input
62
+ # The resulting `Array` is returned via the provided callback.
63
+ #
64
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
65
+ #
66
+ # @param [String, #read, Hash, Array] input
39
67
  # The JSON-LD object to copy and perform the expansion upon.
40
- # @param [IO, Hash, Array] context
68
+ # @param [String, #read, Hash, Array] context
41
69
  # An external context to use additionally to the context embedded in input when expanding the input.
70
+ # @param [Proc] callback (&block)
71
+ # Alternative to using block, with same parameters.
42
72
  # @param [Hash{Symbol => Object}] options
73
+ # @option options [Boolean] :base
74
+ # Base IRI to use when processing relative IRIs.
43
75
  # @raise [InvalidContext]
44
- # @return [Hash, Array]
76
+ # @yield jsonld
77
+ # @yieldparam [Array<Hash>] jsonld
78
+ # The expanded JSON-LD document
79
+ # @return [Array<Hash>]
45
80
  # The expanded JSON-LD document
46
81
  # @see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
47
- def self.expand(input, context = nil, options = {})
82
+ def self.expand(input, context = nil, callback = nil, options = {})
48
83
  result = nil
49
84
  API.new(input, context, options) do |api|
50
85
  result = api.expand(api.value, nil, api.context)
51
86
  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
87
 
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
88
+ # If, after the algorithm outlined above is run, the resulting element is an
89
+ # JSON object with just a @graph property, element is set to the value of @graph's value.
90
+ result = result['@graph'] if result.is_a?(Hash) && result.keys == %w(@graph)
91
+
92
+ # Finally, if element is a JSON object, it is wrapped into an array.
93
+ result = [result] unless result.is_a?(Array)
94
+ callback.call(result) if callback
95
+ yield result if block_given?
96
+ result
121
97
  end
122
98
 
123
99
  ##
@@ -126,194 +102,214 @@ module JSON::LD
126
102
  #
127
103
  # If no context is provided, the input document is compacted using the top-level context of the document
128
104
  #
129
- # @param [IO, Hash, Array] input
105
+ # The resulting `Hash` is returned via the provided callback.
106
+ #
107
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
108
+ #
109
+ # @param [String, #read, Hash, Array] input
130
110
  # The JSON-LD object to copy and perform the compaction upon.
131
- # @param [IO, Hash, Array] context
111
+ # @param [String, #read, Hash, Array] context
132
112
  # The base context to use when compacting the input.
113
+ # @param [Proc] callback (&block)
114
+ # Alternative to using block, with same parameters.
133
115
  # @param [Hash{Symbol => Object}] options
134
- # @raise [InvalidContext, ProcessingError]
116
+ # Other options passed to {#expand}
117
+ # @option options [Boolean] :optimize (false)
118
+ # Perform further optimmization of the compacted output.
119
+ # (Presently, this is a noop).
120
+ # @param [Hash{Symbol => Object}] options
121
+ # @yield jsonld
122
+ # @yieldparam [Hash] jsonld
123
+ # The compacted JSON-LD document
135
124
  # @return [Hash]
136
125
  # The compacted JSON-LD document
126
+ # @raise [InvalidContext, ProcessingError]
137
127
  # @see http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
138
- def self.compact(input, context = nil, options = {})
128
+ def self.compact(input, context, callback = nil, options = {})
139
129
  expanded = result = nil
140
130
 
141
- API.new(input, nil, options) do |api|
142
- expanded = api.expand(api.value, nil, api.context)
131
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
132
+ # This removes any existing context to allow the given context to be cleanly applied.
133
+ API.new(input, nil, options) do
134
+ expanded = expand(value, nil, self.context)
135
+ debug(".compact") {"expanded input: #{value.to_json(JSON_STATE)}"}
136
+
137
+ # x) If no context provided, use context from input document
138
+ context ||= value.fetch('@context', nil)
143
139
  end
144
140
 
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)
141
+ API.new(expanded, context, options) do
142
+ result = compact(value, nil)
150
143
 
151
144
  # xxx) Add the given context to the output
152
145
  result = case result
153
- when Hash then api.context.serialize.merge(result)
154
- when Array then api.context.serialize.merge("@id" => result)
146
+ when Hash then self.context.serialize.merge(result)
147
+ when Array
148
+ kwgraph = self.context.compact_iri('@graph', :quiet => true)
149
+ self.context.serialize.merge(kwgraph => result)
150
+ when String
151
+ kwid = self.context.compact_iri('@id', :quiet => true)
152
+ self.context.serialize.merge(kwid => result)
155
153
  end
156
154
  end
155
+ callback.call(result) if callback
156
+ yield result if block_given?
157
157
  result
158
158
  end
159
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
160
  ##
250
161
  # Frames the given input using the frame according to the steps in the Framing Algorithm. The input is used to build the
251
162
  # framed output and is returned if there are no errors. If there are no matches for the frame, null must be returned.
252
163
  # Exceptions must be thrown if there are errors.
253
164
  #
254
- # @param [IO, Hash, Array] input
165
+ # The resulting `Array` is returned via the provided callback.
166
+ #
167
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
168
+ #
169
+ # @param [String, #read, Hash, Array] input
255
170
  # The JSON-LD object to copy and perform the framing on.
256
- # @param [IO, Hash, Array] frame
171
+ # @param [String, #read, Hash, Array] frame
257
172
  # The frame to use when re-arranging the data.
173
+ # @param [Proc] callback (&block)
174
+ # Alternative to using block, with same parameters.
258
175
  # @param [Hash{Symbol => Object}] options
259
- # @raise [InvalidFrame]
260
- # @return [Hash]
176
+ # Other options passed to {#expand}
177
+ # @option options [Boolean] :embed (true)
178
+ # a flag specifying that objects should be directly embedded in the output,
179
+ # instead of being referred to by their IRI.
180
+ # @option options [Boolean] :explicit (false)
181
+ # a flag specifying that for properties to be included in the output,
182
+ # they must be explicitly declared in the framing context.
183
+ # @option options [Boolean] :omitDefault (false)
184
+ # a flag specifying that properties that are missing from the JSON-LD
185
+ # input should be omitted from the output.
186
+ # @yield jsonld
187
+ # @yieldparam [Hash] jsonld
261
188
  # The framed JSON-LD document
262
- def self.frame(input, frame, options = {})
263
- end
189
+ # @return [Array<Hash>]
190
+ # The framed JSON-LD document
191
+ # @raise [InvalidFrame]
192
+ # @see http://json-ld.org/spec/latest/json-ld-api/#framing-algorithm
193
+ def self.frame(input, frame, callback = nil, options = {})
194
+ result = nil
195
+ match_limit = 0
196
+ framing_state = {
197
+ :embed => true,
198
+ :explicit => false,
199
+ :omitDefault => false,
200
+ :embeds => nil,
201
+ }
202
+ framing_state[:embed] = options[:embed] if options.has_key?(:embed)
203
+ framing_state[:explicit] = options[:explicit] if options.has_key?(:explicit)
204
+ framing_state[:omitDefault] = options[:omitDefault] if options.has_key?(:omitDefault)
264
205
 
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 = {})
206
+ # de-reference frame to create the framing object
207
+ frame = case frame
208
+ when Hash then frame.dup
209
+ when IO, StringIO then JSON.parse(frame.read)
210
+ when String
211
+ content = nil
212
+ RDF::Util::File.open_file(frame) {|f| content = JSON.parse(f.read)}
213
+ content
214
+ end
215
+
216
+ # Expand frame to simplify processing
217
+ expanded_frame = API.expand(frame)
218
+
219
+ # Expand input to simplify processing
220
+ expanded_input = API.expand(input)
221
+
222
+ # Initialize input using frame as context
223
+ API.new(expanded_input, nil, options) do
224
+ debug(".frame") {"context from frame: #{context.inspect}"}
225
+ debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE)}"}
226
+ debug(".frame") {"expanded input: #{value.to_json(JSON_STATE)}"}
227
+
228
+ # Get framing subjects from expanded input, replacing Blank Node identifiers as necessary
229
+ @subjects = Hash.ordered
230
+ depth {get_framing_subjects(@subjects, value, BlankNodeNamer.new("t"))}
231
+ debug(".frame") {"subjects: #{@subjects.to_json(JSON_STATE)}"}
232
+
233
+ result = []
234
+ frame(framing_state, @subjects, expanded_frame[0], result, nil)
235
+ debug(".frame") {"after frame: #{result.to_json(JSON_STATE)}"}
236
+
237
+ # Initalize context from frame
238
+ @context = depth {@context.parse(frame['@context'])}
239
+ # Compact result
240
+ compacted = depth {compact(result, nil)}
241
+ compacted = [compacted] unless compacted.is_a?(Array)
242
+
243
+ # Add the given context to the output
244
+ kwgraph = context.compact_iri('@graph', :quiet => true)
245
+ result = context.serialize.merge({kwgraph => compacted})
246
+ debug(".frame") {"after compact: #{result.to_json(JSON_STATE)}"}
247
+ result = cleanup_preserve(result)
248
+ end
249
+
250
+ callback.call(result) if callback
251
+ yield result if block_given?
252
+ result
278
253
  end
279
254
 
280
255
  ##
281
- # Processes the input according to the RDF Conversion Algorithm, calling the provided tripleCallback for each triple generated.
256
+ # Processes the input according to the RDF Conversion Algorithm, calling the provided callback for each triple generated.
257
+ #
258
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
282
259
  #
283
- # @param [IO, Hash, Array] input
284
- # The JSON-LD object to process when outputting triples.
285
- # @param [IO, Hash, Array] context
260
+ # @param [String, #read, Hash, Array] input
261
+ # The JSON-LD object to process when outputting statements.
262
+ # @param [String, #read, Hash, Array] context
286
263
  # An external context to use additionally to the context embedded in input when expanding the input.
264
+ # @param [Proc] callback (&block)
265
+ # Alternative to using block, with same parameteres.
266
+ # @param [{Symbol,String => Object}] options
267
+ # Options passed to {#expand}
287
268
  # @param [Hash{Symbol => Object}] options
288
269
  # @raise [InvalidContext]
289
270
  # @yield statement
290
271
  # @yieldparam [RDF::Statement] statement
291
- # @return [Hash]
292
- # The normalized JSON-LD document
293
- def self.triples(input, object, context = nil, options = {})
272
+ def self.toRDF(input, context = nil, callback = nil, options = {})
273
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
274
+ # This removes any existing context to allow the given context to be cleanly applied.
275
+ expanded = expand(input, context, nil, options)
276
+
277
+ API.new(expanded, nil, options) do
278
+ debug(".expand") {"expanded input: #{value.to_json(JSON_STATE)}"}
279
+ # Start generating statements
280
+ statements("", value, nil, nil, nil) do |statement|
281
+ callback.call(statement) if callback
282
+ yield statement if block_given?
283
+ end
284
+ end
294
285
  end
295
286
 
296
- private
297
- # Add debug event to debug array, if specified
287
+ ##
288
+ # Take an ordered list of RDF::Statements and turn them into a JSON-LD document.
298
289
  #
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
290
+ # The resulting `Array` is returned via the provided callback.
291
+ #
292
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
293
+ #
294
+ # @param [Array<RDF::Statement>] input
295
+ # @param [Proc] callback (&block)
296
+ # Alternative to using block, with same parameteres.
297
+ # @param [Hash{Symbol => Object}] options
298
+ # @yield jsonld
299
+ # @yieldparam [Hash] jsonld
300
+ # The JSON-LD document in expanded form
301
+ # @return [Array<Hash>]
302
+ # The JSON-LD document in expanded form
303
+ def self.fromRDF(input, callback = nil, options = {})
304
+ result = nil
305
+
306
+ API.new(nil, nil, options) do |api|
307
+ result = api.from_statements(input, BlankNodeNamer.new("t"))
308
+ end
309
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
310
+ callback.call(result) if callback
311
+ yield result if block_given?
312
+ result
317
313
  end
318
314
  end
319
315
  end