json-ld 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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