json-ld 0.9.1 → 1.0.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.
Files changed (45) hide show
  1. data/{README.markdown → README.md} +15 -3
  2. data/VERSION +1 -1
  3. data/lib/json/ld.rb +50 -87
  4. data/lib/json/ld/api.rb +85 -96
  5. data/lib/json/ld/compact.rb +103 -170
  6. data/lib/json/ld/context.rb +1137 -0
  7. data/lib/json/ld/expand.rb +212 -171
  8. data/lib/json/ld/extensions.rb +17 -1
  9. data/lib/json/ld/flatten.rb +145 -78
  10. data/lib/json/ld/frame.rb +1 -1
  11. data/lib/json/ld/from_rdf.rb +73 -103
  12. data/lib/json/ld/reader.rb +3 -1
  13. data/lib/json/ld/resource.rb +3 -3
  14. data/lib/json/ld/to_rdf.rb +98 -109
  15. data/lib/json/ld/utils.rb +54 -4
  16. data/lib/json/ld/writer.rb +5 -5
  17. data/spec/api_spec.rb +3 -28
  18. data/spec/compact_spec.rb +76 -113
  19. data/spec/{evaluation_context_spec.rb → context_spec.rb} +307 -563
  20. data/spec/expand_spec.rb +163 -187
  21. data/spec/flatten_spec.rb +119 -114
  22. data/spec/frame_spec.rb +5 -5
  23. data/spec/from_rdf_spec.rb +44 -24
  24. data/spec/suite_compact_spec.rb +11 -8
  25. data/spec/suite_error_expand_spec.rb +23 -0
  26. data/spec/suite_expand_spec.rb +3 -7
  27. data/spec/suite_flatten_spec.rb +3 -3
  28. data/spec/suite_frame_spec.rb +6 -6
  29. data/spec/suite_from_rdf_spec.rb +3 -3
  30. data/spec/suite_helper.rb +13 -6
  31. data/spec/suite_to_rdf_spec.rb +16 -10
  32. data/spec/test-files/test-1-rdf.ttl +4 -3
  33. data/spec/test-files/test-3-rdf.ttl +2 -1
  34. data/spec/test-files/test-4-compacted.json +1 -1
  35. data/spec/test-files/test-5-rdf.ttl +3 -2
  36. data/spec/test-files/test-6-rdf.ttl +3 -2
  37. data/spec/test-files/test-7-compacted.json +3 -3
  38. data/spec/test-files/test-7-expanded.json +3 -3
  39. data/spec/test-files/test-7-rdf.ttl +7 -6
  40. data/spec/test-files/test-9-compacted.json +1 -1
  41. data/spec/to_rdf_spec.rb +67 -75
  42. data/spec/writer_spec.rb +2 -0
  43. metadata +36 -24
  44. checksums.yaml +0 -15
  45. data/lib/json/ld/evaluation_context.rb +0 -984
@@ -49,7 +49,9 @@ module JSON::LD
49
49
  # @private
50
50
  # @see RDF::Reader#each_statement
51
51
  def each_statement(&block)
52
- JSON::LD::API.toRDF(@doc, @options[:context], nil, @options, &block)
52
+ JSON::LD::API.toRDF(@doc, @options[:context], @options).each do |statement|
53
+ block.call(statement)
54
+ end
53
55
  end
54
56
 
55
57
  ##
@@ -11,7 +11,7 @@ module JSON::LD
11
11
  attr_reader :id
12
12
 
13
13
  # @!attribute [r] context
14
- # @return [JSON::LD::EvaluationContext] Context associated with this resource
14
+ # @return [JSON::LD::Context] Context associated with this resource
15
15
  attr_reader :context
16
16
 
17
17
  # Is this resource clean (i.e., saved to mongo?)
@@ -51,9 +51,9 @@ module JSON::LD
51
51
  # Manage contexts used by resources.
52
52
  #
53
53
  # @param [String] ctx
54
- # @return [JSON::LD::EvaluationContext]
54
+ # @return [JSON::LD::Context]
55
55
  def self.set_context(ctx)
56
- (@@contexts ||= {})[ctx] = JSON::LD::EvaluationContext.new.parse(ctx)
56
+ (@@contexts ||= {})[ctx] = JSON::LD::Context.new.parse(ctx)
57
57
  end
58
58
 
59
59
  # A new resource from the parsed graph
@@ -7,145 +7,134 @@ module JSON::LD
7
7
 
8
8
  ##
9
9
  #
10
- # @param [String] path
11
- # location within JSON hash
12
- # @param [Hash, Array, String] element
13
- # The current JSON element being processed
14
- # @param [RDF::Node] subject
15
- # Inherited subject
16
- # @param [RDF::URI] property
17
- # Inherited property
18
- # @param [RDF::Node] name
19
- # Inherited inherited graph name
20
- # @return [RDF::Resource] defined by this element
21
- # @yield statement
22
- # @yieldparam [RDF::Statement] statement
23
- def statements(path, element, subject, property, name, &block)
24
- debug(path) {"statements: e=#{element.inspect}, s=#{subject.inspect}, p=#{property.inspect}, n=#{name.inspect}"}
10
+ # @param [Hash{String => Hash}] active_graph
11
+ # A hash of IRI to Node definitions
12
+ # @return [Array<RDF::Statement>] statements in this graph, without context
13
+ def graph_to_rdf(active_graph)
14
+ debug('graph_to_rdf') {"graph_to_rdf: #{active_graph.inspect}"}
25
15
 
26
- traverse_result = depth do
27
- case element
28
- when Hash
29
- # Other shortcuts to allow use of this method for terminal associative arrays
30
- object = if element.has_key?('@value')
31
- # 1.2) If the JSON object has a @value key, set the active object to a literal value as follows ...
32
- literal_opts = {}
33
- literal_opts[:datatype] = RDF::URI(element['@type']) if element['@type']
34
- literal_opts[:language] = element['@language'].to_sym if element['@language']
35
- RDF::Literal.new(element['@value'], literal_opts)
36
- elsif element.has_key?('@list')
37
- # 1.3 (Lists)
38
- parse_list("#{path}[#{'@list'}]", element['@list'], property, name, &block)
39
- end
16
+ # Initialize results as an empty array
17
+ results = []
40
18
 
41
- if object
42
- # 1.4
43
- add_quad(path, subject, property, object, name, &block) if subject && property
44
- return object
45
- end
46
-
47
- active_subject = if element.fetch('@id', nil).is_a?(String)
48
- # 1.5 Subject
49
- # 1.5.1 Set active object (subject)
50
- context.expand_iri(element['@id'], :quiet => true)
51
- else
52
- # 1.6) Generate a blank node identifier and set it as the active subject.
53
- node
54
- end
19
+ depth do
20
+ # For each id-node in active_graph
21
+ active_graph.each do |id, node|
22
+ # Initialize subject as the IRI or BNode representation of id
23
+ subject = as_resource(id)
24
+ debug("graph_to_rdf") {"subject: #{subject.to_ntriples}"}
55
25
 
56
- # 1.7) For each key in the JSON object that has not already been processed,
57
- # perform the following steps:
58
- element.keys.kw_sort.each do |key|
59
- value = element[key]
60
- active_property = case key
26
+ # For each property-values in node
27
+ node.each do |property, values|
28
+ case property
61
29
  when '@type'
62
- # If the key is @type, set the active property to rdf:type.
63
- RDF.type
64
- when '@graph'
65
- # Otherwise, if property is @graph, process value algorithm recursively, using active subject
66
- # as graph name and null values for active subject and active property and then continue to
67
- # next property
68
- statements("#{path}[#{key}]", value, nil, nil, (active_subject unless active_subject.node?), &block)
69
- next
30
+ # If property is @type, construct triple as an RDF Triple composed of id, rdf:type, and object from values where id and object are represented either as IRIs or Blank Nodes
31
+ results += values.map do |value|
32
+ object = as_resource(value)
33
+ debug("graph_to_rdf") {"type: #{object.to_ntriples}"}
34
+ RDF::Statement.new(subject, RDF.type, object)
35
+ end
70
36
  when /^@/
71
- # Otherwise, if property is a keyword, skip this step.
72
- next
37
+ # Otherwise, if @type is any other keyword, skip to the next property-values pair
73
38
  else
74
- # 1.7.1) If a key that is not @id, @graph, or @type, set the active property by
75
- # performing Property Processing on the key.
76
- context.expand_iri(key, :quiet => true)
77
- end
39
+ # Otherwise, property is an IRI or Blank Node identifier
40
+ # Initialize predicate from property as an IRI or Blank node
41
+ predicate = as_resource(property)
42
+ debug("graph_to_rdf") {"predicate: #{predicate.to_ntriples}"}
78
43
 
79
- debug("statements[Step 1.7.4]")
80
- statements("#{path}[#{key}]", value, active_subject, active_property, name, &block)
81
- end
82
-
83
- # 1.8) The active_subject is returned
84
- active_subject
85
- when Array
86
- # 2) If a regular array is detected ...
87
- debug("statements[Step 2]")
88
- element.each_with_index do |v, i|
89
- statements("#{path}[#{i}]", v, subject, property, name, &block)
44
+ # For each item in values
45
+ values.each do |item|
46
+ if item.has_key?('@list')
47
+ debug("graph_to_rdf") {"list: #{item.inspect}"}
48
+ # If item is a list object, initialize list_results as an empty array, and object to the result of the List Conversion algorithm, passing the value associated with the @list key from item and list_results.
49
+ list_results = []
50
+ object = parse_list(item['@list'], list_results)
51
+
52
+ # Append a triple composed of subject, prediate, and object to results and add all triples from list_results to results.
53
+ results << RDF::Statement.new(subject, predicate, object)
54
+ results += list_results
55
+ else
56
+ # Otherwise, item is a value object or a node definition. Generate object as the result of the Object Converstion algorithm passing item.
57
+ object = parse_object(item)
58
+ debug("graph_to_rdf") {"object: #{object.to_ntriples}"}
59
+ # Append a triple composed of subject, prediate, and literal to results.
60
+ results << RDF::Statement.new(subject, predicate, object)
61
+ end
62
+ end
63
+ end
90
64
  end
91
- nil # No real value returned from an array
92
- when String
93
- object = RDF::Literal.new(element)
94
- debug(path) {"statements[Step 3]: plain: #{object.inspect}"}
95
- object
96
- when Float
97
- object = RDF::Literal::Double.new(element)
98
- debug(path) {"statements[Step 4]: native: #{object.inspect}"}
99
- object
100
- when Fixnum
101
- object = RDF::Literal.new(element)
102
- debug(path) {"statements[Step 5]: native: #{object.inspect}"}
103
- object
65
+ end
66
+ end
67
+
68
+ # Return results
69
+ results
70
+ end
71
+
72
+ ##
73
+ # Parse an item, either a value object or a node definition
74
+ # @param [Hash] item
75
+ # @return [RDF::Value]
76
+ def parse_object(item)
77
+ if item.has_key?('@value')
78
+ # Otherwise, if element is a JSON object that contains the key @value
79
+ # Initialize value to the value associated with the @value key in element. Initialize datatype to the value associated with the @type key in element, or null if element does not contain that key.
80
+ value, datatype = item.fetch('@value'), item.fetch('@type', nil)
81
+
82
+ case value
104
83
  when TrueClass, FalseClass
105
- object = RDF::Literal::Boolean.new(element)
106
- debug(path) {"statements[Step 6]: native: #{object.inspect}"}
107
- object
84
+ # If value is true or false, then set value its canonical lexical form as defined in the section Data Round Tripping. If datatype is null, set it to xsd:boolean.
85
+ value = value.to_s
86
+ datatype ||= RDF::XSD.boolean.to_s
87
+ when Float, Fixnum
88
+ # Otherwise, if value is a number, then set value to its canonical lexical form as defined in the section Data Round Tripping. If datatype is null, set it to either xsd:integer or xsd:double, depending on if the value contains a fractional and/or an exponential component.
89
+ lit = RDF::Literal.new(value, :canonicalize => true)
90
+ value = lit.to_s
91
+ datatype ||= lit.datatype
108
92
  else
109
- raise RDF::ReaderError, "Traverse to unknown element: #{element.inspect} of type #{element.class}"
93
+ # Otherwise, if datatype is null, set it to xsd:string or rdf:langString, depending on if item has a @language key.
94
+ datatype ||= item.has_key?('@language') ? RDF.langString : RDF::XSD.string
110
95
  end
111
- end
96
+
97
+ # Initialize literal as an RDF literal using value and datatype. If element has the key @language and datatype is xsd:string, then add the value associated with the @language key as the language of the object.
98
+ language = item.fetch('@language', nil)
99
+ literal = RDF::Literal.new(value, :datatype => datatype, :language => language)
112
100
 
113
- # Yield and return traverse_result
114
- add_quad(path, subject, property, traverse_result, name, &block) if subject && property && traverse_result
115
- traverse_result
101
+ # Return literal
102
+ literal
103
+ else
104
+ # Otherwise, value must be a node definition containing only @id whos value is an IRI or Blank Node identifier
105
+ raise "Expected node reference, got #{item.inspect}" unless item.keys == %w(@id)
106
+ # Return value associated with @id as an IRI or Blank node
107
+ as_resource(item['@id'])
108
+ end
116
109
  end
117
110
 
118
111
  ##
119
112
  # Parse a List
120
113
  #
121
- # @param [String] path
122
- # location within JSON hash
123
114
  # @param [Array] list
124
115
  # The Array to serialize as a list
125
- # @param [RDF::URI] property
126
- # Inherited property
127
- # @param [RDF::Resource] name
128
- # Inherited named graph context
116
+ # @param [Array<RDF::Statement>] list_results
117
+ # Statements for each item in the list
129
118
  # @return [RDF::Resource] BNode or nil for head of list
130
- # @yield statement
131
- # @yieldparam [RDF::Statement] statement
132
- def parse_list(path, list, property, name, &block)
133
- debug(path) {"list: #{list.inspect}, p=#{property.inspect}, n=#{name.inspect}"}
119
+ def parse_list(list, list_results)
120
+ debug('parse_list') {"list: #{list.inspect}"}
134
121
 
135
122
  last = list.pop
136
123
  result = first_bnode = last ? node : RDF.nil
137
124
 
138
125
  depth do
139
126
  list.each do |list_item|
140
- # Traverse the value
141
- statements("#{path}", list_item, first_bnode, RDF.first, name, &block)
127
+ # Set first to the result of the Object Converstion algorithm passing item.
128
+ object = parse_object(list_item)
129
+ list_results << RDF::Statement.new(first_bnode, RDF.first, object)
142
130
  rest_bnode = node
143
- add_quad("#{path}", first_bnode, RDF.rest, rest_bnode, name, &block)
131
+ list_results << RDF::Statement.new(first_bnode, RDF.rest, rest_bnode)
144
132
  first_bnode = rest_bnode
145
133
  end
146
134
  if last
147
- statements("#{path}", last, first_bnode, RDF.first, name, &block)
148
- add_quad("#{path}", first_bnode, RDF.rest, RDF.nil, name, &block)
135
+ object = parse_object(last)
136
+ list_results << RDF::Statement.new(first_bnode, RDF.first, object)
137
+ list_results << RDF::Statement.new(first_bnode, RDF.rest, RDF.nil)
149
138
  end
150
139
  end
151
140
  result
@@ -27,7 +27,12 @@ module JSON::LD
27
27
  # @param [Object] value
28
28
  # @return [Boolean]
29
29
  def blank_node?(value)
30
- (node?(value) || node_reference?(value)) && value.fetch('@id', '_:')[0,2] == '_:'
30
+ case value
31
+ when nil then true
32
+ when String then value[0,2] == '_:'
33
+ else
34
+ (node?(value) || node_reference?(value)) && value.fetch('@id', '_:')[0,2] == '_:'
35
+ end
31
36
  end
32
37
 
33
38
  ##
@@ -57,17 +62,62 @@ module JSON::LD
57
62
  value.is_a?(Hash) && value.has_key?('@value')
58
63
  end
59
64
 
65
+ ##
66
+ # Represent an id as an IRI or Blank Node
67
+ # @param [String] id
68
+ # @return [RDF::Resource]
69
+ def as_resource(id)
70
+ @nodes ||= {} # Re-use BNodes
71
+ id[0,2] == '_:' ? (@nodes[id] ||= RDF::Node.new(id[2..-1])) : RDF::URI(id)
72
+ end
73
+
60
74
  private
61
75
 
76
+ # Merge the last value into an array based for the specified key if hash is not null and value is not already in that array
77
+ def merge_value(hash, key, value)
78
+ return unless hash
79
+ values = hash[key] ||= []
80
+ if key == '@list'
81
+ values << value
82
+ elsif list?(value)
83
+ values << value
84
+ elsif !values.include?(value)
85
+ values << value
86
+ end
87
+ end
88
+
89
+ # Merge values into compacted results, creating arrays if necessary
90
+ def merge_compacted_value(hash, key, value)
91
+ return unless hash
92
+ case hash[key]
93
+ when nil then hash[key] = value
94
+ when Array
95
+ if value.is_a?(Array)
96
+ hash[key].concat(value)
97
+ else
98
+ hash[key] << value
99
+ end
100
+ else
101
+ hash[key] = [hash[key]]
102
+ if value.is_a?(Array)
103
+ hash[key].concat(value)
104
+ else
105
+ hash[key] << value
106
+ end
107
+ end
108
+ end
109
+
62
110
  # Add debug event to debug array, if specified
63
111
  #
64
112
  # param [String] message
65
113
  # yieldreturn [String] appended to message, to allow for lazy-evaulation of message
66
114
  def debug(*args)
115
+ options = args.last.is_a?(Hash) ? args.pop : {}
67
116
  return unless ::JSON::LD.debug? || @options[:debug]
117
+ depth = options[:depth] || @depth || 0
68
118
  list = args
69
119
  list << yield if block_given?
70
- message = " " * (@depth || 0) * 2 + (list.empty? ? "" : list.join(": "))
120
+ message = " " * depth * 2 + (list.empty? ? "" : list.join(": "))
71
121
  puts message if JSON::LD::debug?
72
122
  @options[:debug] << message if @options[:debug].is_a?(Array)
73
123
  end
@@ -76,9 +126,9 @@ module JSON::LD
76
126
  def depth(options = {})
77
127
  old_depth = @depth || 0
78
128
  @depth = (options[:depth] || old_depth) + 1
79
- ret = yield
129
+ yield
130
+ ensure
80
131
  @depth = old_depth
81
- ret
82
132
  end
83
133
  end
84
134
 
@@ -59,7 +59,7 @@ module JSON::LD
59
59
  attr_reader :graph
60
60
 
61
61
  # @!attribute [r] context
62
- # @return [EvaluationContext] context used to load and administer contexts
62
+ # @return [Context] context used to load and administer contexts
63
63
  attr_reader :context
64
64
 
65
65
  ##
@@ -83,7 +83,7 @@ module JSON::LD
83
83
  # the prefix mappings to use (not supported by all writers)
84
84
  # @option options [Boolean] :standard_prefixes (false)
85
85
  # Add standard prefixes to @prefixes, if necessary.
86
- # @option options [IO, Array, Hash, String, EvaluationContext] :context (Hash.ordered)
86
+ # @option options [IO, Array, Hash, String, Context] :context (Hash.ordered)
87
87
  # context to use when serializing. Constructed context for native serialization.
88
88
  # @yield [writer] `self`
89
89
  # @yieldparam [RDF::Writer] writer
@@ -149,13 +149,13 @@ module JSON::LD
149
149
  # Turn graph into a triple array
150
150
  statements = @repo.each_statement.to_a
151
151
  debug("writer") { "serialize #{statements.length} statements, #{@options.inspect}"}
152
- result = API.fromRDF(statements, nil, @options)
152
+ result = API.fromRDF(statements, @options)
153
153
 
154
154
  # If we were provided a context, or prefixes, use them to compact the output
155
155
  context = RDF::Util::File.open_file(@options[:context]) if @options[:context].is_a?(String)
156
156
  context ||= @options[:context]
157
157
  context ||= if @options[:prefixes] || @options[:language] || @options[:standard_prefixes]
158
- ctx = EvaluationContext.new(@options)
158
+ ctx = Context.new(@options)
159
159
  ctx.language = @options[:language] if @options[:language]
160
160
  @options[:prefixes].each do |prefix, iri|
161
161
  ctx.set_mapping(prefix, iri) if prefix && iri
@@ -166,7 +166,7 @@ module JSON::LD
166
166
  # Perform compaction, if we have a context
167
167
  if context
168
168
  debug("writer") { "compact result"}
169
- result = API.compact(result, context, nil, @options)
169
+ result = API.compact(result, context, @options)
170
170
  end
171
171
 
172
172
  @output.write(result.to_json(JSON_STATE))
@@ -5,28 +5,6 @@ require 'spec_helper'
5
5
  describe JSON::LD::API do
6
6
  before(:each) { @debug = []}
7
7
 
8
- context "callbacks" do
9
- describe ".compact" do
10
- it "needs to be implemented"
11
- end
12
-
13
- describe ".expand" do
14
- it "needs to be implemented"
15
- end
16
-
17
- describe ".frame" do
18
- it "needs to be implemented"
19
- end
20
-
21
- describe ".fromRDF" do
22
- it "needs to be implemented"
23
- end
24
-
25
- describe ".toRDF" do
26
- it "needs to be implemented"
27
- end
28
- end
29
-
30
8
  context "Test Files" do
31
9
  Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), 'test-files/*-input.*'))) do |filename|
32
10
  test = File.basename(filename).sub(/-input\..*$/, '')
@@ -39,21 +17,18 @@ describe JSON::LD::API do
39
17
  ttl = filename.sub(/-input\..*$/, '-rdf.ttl')
40
18
 
41
19
  context test do
42
- before(:all) do
43
- end
44
-
45
20
  it "expands" do
46
- jld = JSON::LD::API.expand(File.open(filename), (File.open(context) if context), nil, :debug => @debug)
21
+ jld = JSON::LD::API.expand(File.open(filename), (File.open(context) if context), :debug => @debug)
47
22
  jld.should produce(JSON.load(File.open(expanded)), @debug)
48
23
  end if File.exist?(expanded)
49
24
 
50
25
  it "compacts" do
51
- jld = JSON::LD::API.compact(File.open(filename), File.open(context), nil, :debug => @debug)
26
+ jld = JSON::LD::API.compact(File.open(filename), File.open(context), :debug => @debug)
52
27
  jld.should produce(JSON.load(File.open(compacted)), @debug)
53
28
  end if File.exist?(compacted) && File.exist?(context)
54
29
 
55
30
  it "frame" do
56
- jld = JSON::LD::API.frame(File.open(filename), File.open(frame), nil, :debug => @debug)
31
+ jld = JSON::LD::API.frame(File.open(filename), File.open(frame), :debug => @debug)
57
32
  jld.should produce(JSON.load(File.open(framed)), @debug)
58
33
  end if File.exist?(framed) && File.exist?(frame)
59
34