json-ld 0.1.0 → 0.1.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.
@@ -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