json-ld 0.1.6.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,8 @@
1
- === 0.1.6.1
2
- * Merge relative contexts from 0.1.5.2.
1
+ === 0.3.0
2
+ * Fix regression on opening self-relative contexts.
3
+ * When generating RDF, allow @type to be a BNode. This fixes issue #2
4
+ * Add {JSON::LD::Resource} class for simple ruby-like management of JSON-LD nodes.
5
+ * Term Rank and IRI compaction updates.
3
6
 
4
7
  === 0.1.6
5
8
  * Added flattening API, and updated algorithm.
@@ -1,6 +1,7 @@
1
1
  # JSON-LD reader/writer
2
2
 
3
- [JSON-LD][] reader/writer for [RDF.rb][RDF.rb] .
3
+ [![Build Status](https://secure.travis-ci.org/gkellogg/json-ld.png?branch=master)](http://travis-ci.org/gkellogg/json-ld)
4
+ [JSON-LD][] reader/writer for [RDF.rb][RDF.rb] and fully conforming [JSON-LD][] processor.
4
5
 
5
6
  ## Features
6
7
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6.1
1
+ 0.3.0
@@ -29,6 +29,7 @@ module JSON
29
29
  autoload :EvaluationContext, 'json/ld/evaluation_context'
30
30
  autoload :Normalize, 'json/ld/normalize'
31
31
  autoload :Reader, 'json/ld/reader'
32
+ autoload :Resource, 'json/ld/resource'
32
33
  autoload :VERSION, 'json/ld/version'
33
34
  autoload :Writer, 'json/ld/writer'
34
35
 
@@ -33,11 +33,25 @@ module JSON::LD
33
33
  # @param [String, #read, Hash, Array] input
34
34
  # @param [String, #read,, Hash, Array] context
35
35
  # An external context to use additionally to the context embedded in input when expanding the input.
36
- # @param [Hash] options
36
+ # @param [Hash{Symbol => Object}] options
37
+ # @option options [Boolean] :base
38
+ # The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
39
+ # @option options [Boolean] :compactArrays (true)
40
+ # If set to `true`, the JSON-LD processor replaces arrays with just one element with that element during compaction. If set to `false`, all arrays will remain arrays even if they have just one element.
41
+ # @option options [Proc] :conformanceCallback
42
+ # The purpose of this option is to instruct the processor about whether or not it should continue processing. If the value is null, the processor should ignore any key-value pair associated with any recoverable conformance issue and continue processing. More details about this feature can be found in the ConformanceCallback section.
43
+ # @option options [Boolean, String, RDF::URI] :flatten
44
+ # If set to a value that is not `false`, the JSON-LD processor must modify the output of the Compaction Algorithm or the Expansion Algorithm by coalescing all properties associated with each subject via the Flattening Algorithm. The value of `flatten must` be either an _IRI_ value representing the name of the graph to flatten, or `true`. If the value is `true`, then the first graph encountered in the input document is selected and flattened.
45
+ # @option options [Boolean] :optimize (false)
46
+ # If set to `true`, the JSON-LD processor is allowed to optimize the output of the Compaction Algorithm to produce even compacter representations. The algorithm for compaction optimization is beyond the scope of this specification and thus not defined. Consequently, different implementations *MAY* implement different optimization algorithms.
47
+ # (Presently, this is a noop).
48
+ # @option options [Boolean] :useNativeDatatypes (true)
49
+ # If set to `true`, the JSON-LD processor will use the expanded `rdf:type` IRI as the property instead of `@type` when converting from RDF.
50
+ # @option options [Boolean] :useRdfType (false) If set to `true`, the JSON-LD processor will try to convert datatyped literals to JSON native types instead of using the expanded object form when converting from RDF. `xsd:boolean` values will be converted to `true` or `false`. `xsd:integer` and `xsd:double` values will be converted to JSON numbers.
37
51
  # @yield [api]
38
52
  # @yieldparam [API]
39
53
  def initialize(input, context, options = {}, &block)
40
- @options = options
54
+ @options = {:compactArrays => true}.merge(options)
41
55
  @value = case input
42
56
  when Array, Hash then input.dup
43
57
  when IO, StringIO then JSON.parse(input.read)
@@ -72,8 +86,7 @@ module JSON::LD
72
86
  # @param [Proc] callback (&block)
73
87
  # Alternative to using block, with same parameters.
74
88
  # @param [Hash{Symbol => Object}] options
75
- # @option options [Boolean] :base
76
- # Base IRI to use when processing relative IRIs.
89
+ # See options in {#initialize}
77
90
  # @raise [InvalidContext]
78
91
  # @yield jsonld
79
92
  # @yieldparam [Array<Hash>] jsonld
@@ -115,10 +128,8 @@ module JSON::LD
115
128
  # @param [Proc] callback (&block)
116
129
  # Alternative to using block, with same parameters.
117
130
  # @param [Hash{Symbol => Object}] options
131
+ # See options in {#initialize}
118
132
  # Other options passed to {#expand}
119
- # @option options [Boolean] :optimize (false)
120
- # Perform further optimmization of the compacted output.
121
- # (Presently, this is a noop).
122
133
  # @yield jsonld
123
134
  # @yieldparam [Hash] jsonld
124
135
  # The compacted JSON-LD document
@@ -169,16 +180,8 @@ module JSON::LD
169
180
  # @param [Proc] callback (&block)
170
181
  # Alternative to using block, with same parameters.
171
182
  # @param [Hash{Symbol => Object}] options
183
+ # See options in {#initialize}
172
184
  # Other options passed to {#expand}
173
- # @option options [Boolean] :embed (true)
174
- # a flag specifying that objects should be directly embedded in the output,
175
- # instead of being referred to by their IRI.
176
- # @option options [Boolean] :explicit (false)
177
- # a flag specifying that for properties to be included in the output,
178
- # they must be explicitly declared in the framing context.
179
- # @option options [Boolean] :omitDefault (false)
180
- # a flag specifying that properties that are missing from the JSON-LD
181
- # input should be omitted from the output.
182
185
  # @yield jsonld
183
186
  # @yieldparam [Hash] jsonld
184
187
  # The framed JSON-LD document
@@ -240,6 +243,7 @@ module JSON::LD
240
243
  # @param [Proc] callback (&block)
241
244
  # Alternative to using block, with same parameters.
242
245
  # @param [Hash{Symbol => Object}] options
246
+ # See options in {#initialize}
243
247
  # Other options passed to {#expand}
244
248
  # @option options [Boolean] :embed (true)
245
249
  # a flag specifying that objects should be directly embedded in the output,
@@ -338,6 +342,7 @@ module JSON::LD
338
342
  # @param [Proc] callback (&block)
339
343
  # Alternative to using block, with same parameteres.
340
344
  # @param [{Symbol,String => Object}] options
345
+ # See options in {#initialize}
341
346
  # Options passed to {#expand}
342
347
  # @raise [InvalidContext]
343
348
  # @yield statement
@@ -368,14 +373,14 @@ module JSON::LD
368
373
  # @param [Proc] callback (&block)
369
374
  # Alternative to using block, with same parameteres.
370
375
  # @param [Hash{Symbol => Object}] options
371
- # @option options [Boolean] :useRdfType (false) use rdf:type instead of @type
372
- # @option options [Boolean] :useNativeDatatypes FIXME
376
+ # See options in {#initialize}
373
377
  # @yield jsonld
374
378
  # @yieldparam [Hash] jsonld
375
379
  # The JSON-LD document in expanded form
376
380
  # @return [Array<Hash>]
377
381
  # The JSON-LD document in expanded form
378
382
  def self.fromRDF(input, callback = nil, options = {})
383
+ options = {:useNativeTypes => true}.merge(options)
379
384
  result = nil
380
385
 
381
386
  API.new(nil, nil, options) do |api|
@@ -25,7 +25,7 @@ module JSON::LD
25
25
  # If element has a single member and the active property has no
26
26
  # @container mapping to @list or @set, the compacted value is that
27
27
  # member; otherwise the compacted value is element
28
- if result.length == 1
28
+ if result.length == 1 && @options[:compactArrays]
29
29
  debug("=> extract single element: #{result.first.inspect}")
30
30
  result.first
31
31
  else
@@ -44,13 +44,17 @@ module JSON::LD
44
44
 
45
45
  case k
46
46
  when '@value', '@id'
47
- # If element has an @value property or element is a node reference, return the result of performing
48
- # Value Compaction on element using active property.
47
+ # If element has an @value property or element is a node reference, return the result of performing Value Compaction on element using active property.
49
48
  v = context.compact_value(property, element, :depth => @depth)
50
49
  debug("compact") {"value optimization, return as #{v.inspect}"}
51
50
  return v
52
51
  when '@list'
53
- # Otherwise, if the active property has a @container mapping to @list and element has a corresponding @list property, recursively compact that property's value passing a copy of the active context and the active property ensuring that the result is an array and removing null values.
52
+ # Otherwise, if the active property has a @container mapping to @list and element has a corresponding @list property, recursively compact that property's value passing a copy of the active context and the active property ensuring that the result is an array with all null values removed.
53
+
54
+ # If there already exists a value for active property in element and the full IRI of property is also coerced to @list, return an error.
55
+ # FIXME: check for full-iri list coercion
56
+
57
+ # Otherwise store the resulting array as value of active property if empty or property otherwise.
54
58
  compacted_key = context.compact_iri(k, :position => :predicate, :depth => @depth)
55
59
  v = depth { compact(element[k], property) }
56
60
 
@@ -66,18 +70,19 @@ module JSON::LD
66
70
  debug("compact") {"#{key}: #{value.inspect}"}
67
71
 
68
72
  if %(@id @type).include?(key)
73
+ position = key == '@id' ? :subject : :type
69
74
  compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
70
75
 
71
76
  result[compacted_key] = case value
72
77
  when String
73
78
  # If value is a string, the compacted value is the result of performing IRI Compaction on value.
74
79
  debug {" => compacted string for #{key}"}
75
- context.compact_iri(value, :position => :subject, :depth => @depth)
80
+ context.compact_iri(value, :position => position, :depth => @depth)
76
81
  when Array
77
82
  # Otherwise, value must be an array. Perform IRI Compaction on every entry of value. If value contains just one entry, value is set to that entry
78
- compacted_value = value.map {|v| context.compact_iri(v, :position => :subject, :depth => @depth)}
83
+ compacted_value = value.map {|v| context.compact_iri(v, :position => position, :depth => @depth)}
79
84
  debug {" => compacted value(#{key}): #{compacted_value.inspect}"}
80
- compacted_value = compacted_value.first if compacted_value.length == 1
85
+ compacted_value = compacted_value.first if compacted_value.length == 1 && @options[:compactArrays]
81
86
  compacted_value
82
87
  end
83
88
  else
@@ -183,7 +183,7 @@ module JSON::LD
183
183
  new_ec.send(setter, v)
184
184
  elsif v
185
185
  raise InvalidContext::Syntax, "#{key.inspect} is invalid"
186
- end
186
+ end
187
187
  end
188
188
 
189
189
  num_updates = 1
@@ -313,7 +313,7 @@ module JSON::LD
313
313
  debug {"=> coerce(#{k}) => #{coerce(k)}"}
314
314
  if coerce(k) && !NATIVE_DATATYPES.include?(coerce(k))
315
315
  dt = coerce(k)
316
- dt = compact_iri(dt, :position => :datatype) unless dt == '@id'
316
+ dt = compact_iri(dt, :position => :type) unless dt == '@id'
317
317
  # Fold into existing definition
318
318
  ctx[k]["@type"] = dt
319
319
  debug {"=> datatype[#{k}] => #{dt}"}
@@ -478,7 +478,7 @@ module JSON::LD
478
478
  # @param [String] iri
479
479
  # A keyword, term, prefix:suffix or possibly relative IRI
480
480
  # @param [Hash{Symbol => Object}] options
481
- # @option options [:subject, :predicate, :object, :datatype] position
481
+ # @option options [:subject, :predicate, :type] position
482
482
  # Useful when determining how to serialize.
483
483
  # @option options [RDF::URI] base (self.base)
484
484
  # Base IRI to use when expanding relative IRIs.
@@ -491,7 +491,7 @@ module JSON::LD
491
491
  prefix, suffix = iri.split(':', 2)
492
492
  return mapping(iri) if mapping(iri) # If it's an exact match
493
493
  debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{vocab.inspect}"} unless options[:quiet]
494
- base = [:subject, :object].include?(options[:position]) ? options.fetch(:base, self.base) : nil
494
+ base = [:subject].include?(options[:position]) ? options.fetch(:base, self.base) : nil
495
495
  prefix = prefix.to_s
496
496
  case
497
497
  when prefix == '_' && suffix then bnode(suffix)
@@ -503,7 +503,7 @@ module JSON::LD
503
503
  else
504
504
  # Otherwise, it must be an absolute IRI
505
505
  u = uri(iri)
506
- u if u.absolute? || [:subject, :object].include?(options[:position])
506
+ u if u.absolute? || [:subject].include?(options[:position])
507
507
  end
508
508
  end
509
509
 
@@ -512,7 +512,7 @@ module JSON::LD
512
512
  #
513
513
  # @param [RDF::URI] iri
514
514
  # @param [Hash{Symbol => Object}] options ({})
515
- # @option options [:subject, :predicate, :object, :datatype] position
515
+ # @option options [:subject, :predicate, :type] position
516
516
  # Useful when determining how to serialize.
517
517
  # @option options [Object] :value
518
518
  # Value, used to select among various maps for the same IRI
@@ -528,55 +528,54 @@ module JSON::LD
528
528
  value = options.fetch(:value, nil)
529
529
 
530
530
  # Get a list of terms which map to iri
531
- terms = mappings.keys.select {|t| mapping(t).to_s == iri}
531
+ matched_terms = mappings.keys.select {|t| mapping(t).to_s == iri}
532
+ debug("compact_iri", "initial terms: #{matched_terms.inspect}")
532
533
 
533
- # Create an association term map for terms to their associated
534
- # term rank.
535
- term_map = {}
534
+ # Create an empty list of terms _terms_ that will be populated with terms that are ranked according to how closely they match value. Initialize highest rank to 0, and set a flag list container to false.
535
+ terms = {}
536
536
 
537
537
  # If value is a @list add a term rank for each
538
538
  # term mapping to iri which has @container @list.
539
539
  debug("compact_iri", "#{value.inspect} is a list? #{list?(value).inspect}")
540
540
  if list?(value)
541
- list_terms = terms.select {|t| container(t) == '@list'}
541
+ list_terms = matched_terms.select {|t| container(t) == '@list'}
542
542
 
543
- term_map = list_terms.inject({}) do |memo, t|
543
+ terms = list_terms.inject({}) do |memo, t|
544
544
  memo[t] = term_rank(t, value)
545
545
  memo
546
546
  end unless list_terms.empty?
547
- debug("term map") {"remove zero rank terms: #{term_map.keys.select {|t| term_map[t] == 0}}"} if term_map.any? {|t,r| r == 0}
548
- term_map.delete_if {|t, r| r == 0}
547
+ debug("term map") {"remove zero rank terms: #{terms.keys.select {|t| terms[t] == 0}}"} if terms.any? {|t,r| r == 0}
548
+ terms.delete_if {|t, r| r == 0}
549
549
  end
550
550
 
551
551
  # Otherwise, value is @value or a native type.
552
552
  # Add a term rank for each term mapping to iri
553
553
  # which does not have @container @list
554
- if term_map.empty?
555
- non_list_terms = terms.reject {|t| container(t) == '@list'}
554
+ if terms.empty?
555
+ non_list_terms = matched_terms.reject {|t| container(t) == '@list'}
556
556
 
557
557
  # If value is a @list, exclude from term map those terms
558
558
  # with @container @set
559
559
  non_list_terms.reject {|t| container(t) == '@set'} if list?(value)
560
560
 
561
- term_map = non_list_terms.inject({}) do |memo, t|
561
+ terms = non_list_terms.inject({}) do |memo, t|
562
562
  memo[t] = term_rank(t, value)
563
563
  memo
564
564
  end unless non_list_terms.empty?
565
- debug("term map") {"remove zero rank terms: #{term_map.keys.select {|t| term_map[t] == 0}}"} if term_map.any? {|t,r| r == 0}
566
- term_map.delete_if {|t, r| r == 0}
565
+ debug("term map") {"remove zero rank terms: #{terms.keys.select {|t| terms[t] == 0}}"} if terms.any? {|t,r| r == 0}
566
+ terms.delete_if {|t, r| r == 0}
567
567
  end
568
568
 
569
569
  # If we don't want terms, remove anything that's not a CURIE or IRI
570
- term_map.keep_if {|t, v| t.index(':') } if options.fetch(:not_term, false)
570
+ terms.keep_if {|t, v| t.index(':') } if options.fetch(:not_term, false)
571
571
 
572
572
  # Find terms having the greatest term match value
573
- least_distance = term_map.values.max
574
- terms = term_map.keys.select {|t| term_map[t] == least_distance}
573
+ least_distance = terms.values.max
574
+ terms = terms.keys.select {|t| terms[t] == least_distance}
575
575
 
576
- # If terms is empty, and the active context has a @vocab which is a
577
- # prefix of iri where the resulting relative IRI is not a term in the
578
- # active context. The resulting relative IRI is the unmatched part of iri.
579
- if vocab && terms.empty? && iri.to_s.index(vocab) == 0
576
+ # If terms is empty, and the active context has a @vocab which is a prefix of iri where the resulting relative IRI is not a term in the active context. The resulting relative IRI is the unmatched part of iri.
577
+ if vocab && terms.empty? && iri.to_s.index(vocab) == 0 &&
578
+ [:predicate, :type].include?(options[:position])
580
579
  terms << iri.to_s.sub(vocab, '')
581
580
  debug("vocab") {"vocab: #{vocab}, rel: #{terms.first}"}
582
581
  end
@@ -608,7 +607,7 @@ module JSON::LD
608
607
  end
609
608
 
610
609
  terms = curies.select do |curie|
611
- container(curie) != '@list' &&
610
+ (options[:position] != :predicate || container(curie) != '@list') &&
612
611
  coerce(curie).nil? &&
613
612
  language(curie) == default_language
614
613
  end
@@ -755,7 +754,7 @@ module JSON::LD
755
754
  debug("else")
756
755
  case coerce(property)
757
756
  when '@id'
758
- {'@id' => expand_iri(value, :position => :object).to_s}
757
+ {'@id' => expand_iri(value, :position => :subject).to_s}
759
758
  when nil
760
759
  debug("expand value") {"lang(prop): #{language(property).inspect}, def: #{default_language.inspect}"}
761
760
  language(property) ? {"@value" => value.to_s, "@language" => language(property)} : {"@value" => value.to_s}
@@ -792,22 +791,22 @@ module JSON::LD
792
791
  debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
793
792
 
794
793
  result = case
795
- #when %w(boolean integer double).any? {|t| expand_iri(value['@type'], :position => :datatype) == RDF::XSD[t]}
794
+ #when %w(boolean integer double).any? {|t| expand_iri(value['@type'], :position => :type) == RDF::XSD[t]}
796
795
  # # Compact native type
797
796
  # debug {" (native)"}
798
- # l = RDF::Literal(value['@value'], :datatype => expand_iri(value['@type'], :position => :datatype))
797
+ # l = RDF::Literal(value['@value'], :datatype => expand_iri(value['@type'], :position => :type))
799
798
  # l.canonicalize.object
800
799
  when coerce(property) == '@id' && value.has_key?('@id')
801
800
  # Compact an @id coercion
802
801
  debug {" (@id & coerce)"}
803
- compact_iri(value['@id'], :position => :object)
804
- when value['@type'] && expand_iri(value['@type'], :position => :datatype) == coerce(property)
802
+ compact_iri(value['@id'], :position => :subject)
803
+ when value['@type'] && expand_iri(value['@type'], :position => :type) == coerce(property)
805
804
  # Compact common datatype
806
805
  debug {" (@type & coerce) == #{coerce(property)}"}
807
806
  value['@value']
808
807
  when value.has_key?('@id')
809
808
  # Compact an IRI
810
- value[self.alias('@id')] = compact_iri(value['@id'], :position => :object)
809
+ value[self.alias('@id')] = compact_iri(value['@id'], :position => :subject)
811
810
  debug {" (#{self.alias('@id')} => #{value['@id']})"}
812
811
  value
813
812
  when value['@language'] && value['@language'] == language(property)
@@ -829,7 +828,7 @@ module JSON::LD
829
828
  when value['@type']
830
829
  # Compact datatype
831
830
  debug {" (@type)"}
832
- value[self.alias('@type')] = compact_iri(value['@type'], :position => :datatype)
831
+ value[self.alias('@type')] = compact_iri(value['@type'], :position => :type)
833
832
  value
834
833
  else
835
834
  # Otherwise, use original value
@@ -928,7 +927,7 @@ module JSON::LD
928
927
  end
929
928
  elsif value?(value)
930
929
  val_type = value.fetch('@type', nil)
931
- val_lang = value.fetch('@language', nil)
930
+ val_lang = value['@language'] || false if value.has_key?('@language')
932
931
  debug("term rank") {"@val_type: #{val_type.inspect}, val_lang: #{val_lang.inspect}"}
933
932
  if val_type
934
933
  coerce(term) == val_type ? 3 : (default_term ? 1 : 0)
@@ -936,9 +935,15 @@ module JSON::LD
936
935
  default_term ? 2 : 1
937
936
  elsif val_lang.nil?
938
937
  debug("val_lang.nil") {"#{language(term).inspect} && #{coerce(term).inspect}"}
939
- !language(term) && !coerce(term) ? 3 : 0
938
+ language(term) == false || (default_term && default_language.nil?) ? 3 : 0
940
939
  else
941
- val_lang == language(term) ? 3 : (default_term ? 1 : 0)
940
+ if val_lang == language(term) || (default_term && default_language == val_lang)
941
+ 3
942
+ elsif default_term
943
+ 1
944
+ else
945
+ 0
946
+ end
942
947
  end
943
948
  else # node definition/reference
944
949
  coerce(term) == '@id' ? 3 : (default_term ? 1 : 0)
@@ -181,7 +181,7 @@ module JSON::LD
181
181
  end
182
182
  else
183
183
  # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
184
- context.expand_value(active_property, input, :position => :object, :depth => @depth) unless input.nil?
184
+ context.expand_value(active_property, input, :position => :subject, :depth => @depth) unless input.nil?
185
185
  end
186
186
 
187
187
  debug {" => #{result.inspect}"}
@@ -71,6 +71,7 @@ module JSON::LD
71
71
  # append the string representation of object to the array value for the key @type, creating
72
72
  # an entry if necessary
73
73
  (value['@type'] ||= []) << object
74
+ # FIXME: 3.7) If object is a typed literal and the useNativeTypes option is set to true:
74
75
  elsif statement.object == RDF.nil
75
76
  # Otherwise, if object is http://www.w3.org/1999/02/22-rdf-syntax-ns#nil, let
76
77
  # key be the string representation of predicate. Set the value
@@ -139,7 +140,7 @@ module JSON::LD
139
140
  end
140
141
 
141
142
  # Return array as the graph representation.
142
- debug("fromRdf") {array.to_json(JSON_STATE)}
143
+ debug("fromRDF") {array.to_json(JSON_STATE)}
143
144
  array
144
145
  end
145
146
  end
@@ -0,0 +1,243 @@
1
+ module JSON::LD
2
+ # Simple Ruby reflector class to provide native
3
+ # access to JSON-LD objects
4
+ class Resource
5
+ # Object representation of resource
6
+ #
7
+ # @attr_reader [Hash<String => Object] attributes
8
+ attr_reader :attributes
9
+
10
+ # ID of this resource
11
+ #
12
+ # @attr_reader [String] id
13
+ attr_reader :id
14
+
15
+ # Context associated with this resource
16
+ #
17
+ # @attr_reader [JSON::LD::EvaluationContext] context
18
+ attr_reader :context
19
+
20
+ # Is this resource clean (i.e., saved to mongo?)
21
+ #
22
+ # @return [Boolean]
23
+ def clean?; @clean; end
24
+
25
+ # Is this resource dirty (i.e., not yet saved to mongo?)
26
+ #
27
+ # @return [Boolean]
28
+ def dirty?; !clean?; end
29
+
30
+ # Has this resource been reconciled against a mongo ID?
31
+ #
32
+ # @return [Boolean]
33
+ def reconciled?; @reconciled; end
34
+
35
+ # Has this resource been resolved so that
36
+ # all references are to other Resources?
37
+ #
38
+ # @return [Boolean]
39
+ def resolved?; @resolved; end
40
+
41
+ # Anonymous resources have BNode ids or no schema:url
42
+ #
43
+ # @return [Boolean]
44
+ def anonymous?; @anon; end
45
+
46
+ # Is this a stub resource, which has not yet been
47
+ # synched or created within the DB?
48
+ def stub?; !!@stub; end
49
+
50
+ # Is this a new resource, which has not yet been
51
+ # synched or created within the DB?
52
+ def new?; !!@new; end
53
+
54
+ # Manage contexts used by resources.
55
+ #
56
+ # @param [String] ctx
57
+ # @return [JSON::LD::EvaluationContext]
58
+ def self.set_context(ctx)
59
+ (@@contexts ||= {})[ctx] = JSON::LD::EvaluationContext.new.parse(ctx)
60
+ end
61
+
62
+ # A new resource from the parsed graph
63
+ # @param [Hash{String => Object}] node_definition
64
+ # @param [Hash{Symbol => Object}] options
65
+ # @option options [String] :context
66
+ # Resource context, used for finding
67
+ # appropriate collection and JSON-LD context.
68
+ # @option options [Boolean] :clean (false)
69
+ # @option options [Boolean] :compact (false)
70
+ # Assume `node_definition` is in expanded form
71
+ # and compact using `context`.
72
+ # @option options [Boolean] :reconciled (!new)
73
+ # node_definition is not based on Mongo IDs
74
+ # and must be reconciled against Mongo, or merged
75
+ # into another resource.
76
+ # @option options [Boolean] :new (true)
77
+ # This is a new resource, not yet saved to Mongo
78
+ # @option options [Boolean] :stub (false)
79
+ # This is a stand-in for another resource that has
80
+ # not yet been retrieved (or created) from Mongo
81
+ def initialize(node_definition, options = {})
82
+ @context_name = options[:context]
83
+ @context = self.class.set_context(@context_name)
84
+ @clean = options.fetch(:clean, false)
85
+ @new = options.fetch(:new, true)
86
+ @reconciled = options.fetch(:reconciled, !@new)
87
+ @resolved = false
88
+ @attributes = if options[:compact]
89
+ JSON::LD::API.compact(node_definition, @context)
90
+ else
91
+ node_definition
92
+ end
93
+ @id = @attributes['@id']
94
+ @anon = @id.nil? || @id.to_s[0,2] == '_:'
95
+ end
96
+
97
+ # Return a hash of this object, suitable for use by for ETag
98
+ # @return [Fixnum]
99
+ def hash
100
+ self.deresolve.hash
101
+ end
102
+
103
+ # Reverse resolution of resource attributes.
104
+ # Just returns `attributes` if
105
+ # resource is unresolved. Otherwise, replaces `Resource`
106
+ # values with node references.
107
+ #
108
+ # Result is expanded and re-compacted to get to normalized
109
+ # representation.
110
+ #
111
+ # @return [Hash] deresolved attribute hash
112
+ def deresolve
113
+ node_definition = if resolved?
114
+ deresolved = attributes.keys.inject({}) do |memo, prop|
115
+ value = attributes[prop]
116
+ memo[prop] = case value
117
+ when Resource
118
+ {'id' => value.id}
119
+ when Array
120
+ value.map do |v|
121
+ v.is_a?(Resource) ? {'id' => v.id} : v
122
+ end
123
+ else
124
+ value
125
+ end
126
+ memo
127
+ end
128
+ deresolved
129
+ else
130
+ attributes
131
+ end
132
+
133
+ compacted = nil
134
+ JSON::LD::API.expand(node_definition, @context) do |expanded|
135
+ compacted = JSON::LD::API.compact(expanded, @context)
136
+ end
137
+ compacted.delete_if {|k, v| k == '@context'}
138
+ end
139
+
140
+ # Serialize to JSON-LD, minus `@context` using
141
+ # a deresolved version of the attributes
142
+ #
143
+ # @param [Hash] options
144
+ # @return [String] serizlied JSON representation of resource
145
+ def to_json(options = nil)
146
+ deresolve.to_json(options)
147
+ end
148
+
149
+ # Update node references using the provided map.
150
+ # This replaces node references with Resources,
151
+ # either stub or instantiated.
152
+ #
153
+ # Node references with ids not in the reference_map
154
+ # will cause stub resources to be added to the map.
155
+ #
156
+ # @param [Hash{String => Resource}] reference_map
157
+ # @return [Resource] self
158
+ def resolve(reference_map)
159
+ return if resolved?
160
+ def update_obj(obj, reference_map)
161
+ case obj
162
+ when Array
163
+ obj.map {|o| update_obj(o, reference_map)}
164
+ when Hash
165
+ if obj.node_ref?
166
+ reference_map[obj['id']] ||= Resource.new(obj,
167
+ :context => @context_name,
168
+ :clean => false,
169
+ :stub => true
170
+ )
171
+ else
172
+ obj.keys.each do |k|
173
+ obj[k] = update_obj(obj[k], reference_map)
174
+ end
175
+ obj
176
+ end
177
+ else
178
+ obj
179
+ end
180
+ end
181
+
182
+ #$logger.debug "resolve(0): #{attributes.inspect}"
183
+ @attributes.each do |k, v|
184
+ next if %w(id type).include?(k)
185
+ @attributes[k] = update_obj(@attributes[k], reference_map)
186
+ end
187
+ #$logger.debug "resolve(1): #{attributes.inspect}"
188
+ @resolved = true
189
+ self
190
+ end
191
+
192
+ # Merge resources
193
+ # FIXME: If unreconciled or unresolved resources are merged
194
+ # against reconciled/resolved resources, they will appear
195
+ # to not match, even if they are really the same thing.
196
+ #
197
+ # @param [Resource] resource
198
+ # @return [Resource] self
199
+ def merge(resource)
200
+ if attributes.neq?(resource.attributes)
201
+ resource.attributes.each do |p, v|
202
+ next if p == 'id'
203
+ if v.nil? or (v.is_a?(Array) and v.empty?)
204
+ attributes.delete(p)
205
+ else
206
+ attributes[p] = v
207
+ end
208
+ end
209
+ @resolved = @clean = false
210
+ end
211
+ self
212
+ end
213
+
214
+ #
215
+ # Override this method to implement save using
216
+ # an appropriate storage mechanism.
217
+ #
218
+ # Save the object to the Mongo collection
219
+ # use Upsert to create things that don't exist.
220
+ # First makes sure that the resource is valid.
221
+ #
222
+ # @return [Boolean] true or false if resource not saved
223
+ def save
224
+ raise NotImplemented
225
+ end
226
+
227
+ # Access individual fields, from subject definition
228
+ def property(prop_name); @attributes.fetch(prop_name, nil); end
229
+
230
+ # Access individual fields, from subject definition
231
+ def method_missing(method, *args)
232
+ property(method.to_s)
233
+ end
234
+
235
+ def inspect
236
+ "<Resource" +
237
+ attributes.map do |k, v|
238
+ "\n #{k}: #{v.inspect}"
239
+ end.join(" ") +
240
+ ">"
241
+ end
242
+ end
243
+ end