Class: JSON::LD::EvaluationContext

Inherits:
Object
  • Object
show all
Includes:
Utils
Defined in:
lib/json/ld/evaluation_context.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils

#blank_node?, #index?, #list?, #node?, #node_reference?, #value?

Constructor Details

#initialize(options = {}) {|ec| ... } ⇒ EvaluationContext

Create new evaluation context

Yields:

  • (ec)

Yield Parameters:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/json/ld/evaluation_context.rb', line 87

def initialize(options = {})
  @base = RDF::URI(options[:base]) if options[:base]
  @mappings =  {}
  @coercions = {}
  @containers = {}
  @languages = {}
  @iri_to_curie = {}
  @iri_to_term = {
    RDF.to_uri.to_s => "rdf",
    RDF::XSD.to_uri.to_s => "xsd"
  }
  @remote_contexts = []

  @options = options

  # Load any defined prefixes
  (options[:prefixes] || {}).each_pair do |k, v|
    @iri_to_term[v.to_s] = k unless k.nil?
  end

  debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
  
  yield(self) if block_given?
end

Instance Attribute Details

#baseRDF::URI

The base.

Returns:

  • (RDF::URI)

    Document base IRI, used for expanding relative IRIs.



13
14
15
# File 'lib/json/ld/evaluation_context.rb', line 13

def base
  @base
end

#coercionsHash{String => String}

Type coersion

The @type keyword is used to specify type coersion rules for the data. For each key in the map, the key is a String representation of the property for which String values will be coerced and the value is the datatype (or @id) to coerce to. Type coersion for the value ‘@id` asserts that all vocabulary terms listed should undergo coercion to an IRI, including CURIE processing for compact IRI Expressions like `foaf:homepage`.

Returns:

  • (Hash{String => String})


37
38
39
# File 'lib/json/ld/evaluation_context.rb', line 37

def coercions
  @coercions
end

#containersHash{String => String}

List coercion

The @container keyword is used to specify how arrays are to be treated. A value of @list indicates that arrays of values are to be treated as an ordered list. A value of @set indicates that arrays are to be treated as unordered and that singular values are always coerced to an array form on expansion and compaction.

Returns:

  • (Hash{String => String})


44
45
46
# File 'lib/json/ld/evaluation_context.rb', line 44

def containers
  @containers
end

#context_baseRDF::URI

Returns base IRI of the context, if loaded remotely.

Returns:

  • (RDF::URI)

    base IRI of the context, if loaded remotely.



17
18
19
# File 'lib/json/ld/evaluation_context.rb', line 17

def context_base
  @context_base
end

#default_languageString

Default language

This adds a language to plain strings that aren’t otherwise coerced

Returns:

  • (String)


60
61
62
# File 'lib/json/ld/evaluation_context.rb', line 60

def default_language
  @default_language
end

#iri_to_curieHash{RDF::URI => String}

Returns Reverse mappings from IRI to a term or CURIE.

Returns:

  • (Hash{RDF::URI => String})

    Reverse mappings from IRI to a term or CURIE



25
26
27
# File 'lib/json/ld/evaluation_context.rb', line 25

def iri_to_curie
  @iri_to_curie
end

#iri_to_termHash{RDF::URI => String}

Returns Reverse mappings from IRI to term only for terms, not CURIEs.

Returns:

  • (Hash{RDF::URI => String})

    Reverse mappings from IRI to term only for terms, not CURIEs



29
30
31
# File 'lib/json/ld/evaluation_context.rb', line 29

def iri_to_term
  @iri_to_term
end

#languagesHash{String => String}

Language coercion

The @language keyword is used to specify language coercion rules for the data. For each key in the map, the key is a String representation of the property for which String values will be coerced and the value is the language to coerce to. If no property-specific language is given, any default language from the context is used.

Returns:

  • (Hash{String => String})


52
53
54
# File 'lib/json/ld/evaluation_context.rb', line 52

def languages
  @languages
end

#mappingsHash{String => String}

Returns A list of current, in-scope mappings from term to IRI.

Returns:

  • (Hash{String => String})

    A list of current, in-scope mappings from term to IRI.



21
22
23
# File 'lib/json/ld/evaluation_context.rb', line 21

def mappings
  @mappings
end

#optionsHash{Symbol => Object}

Returns Global options used in generating IRIs.

Returns:

  • (Hash{Symbol => Object})

    Global options used in generating IRIs



72
73
74
# File 'lib/json/ld/evaluation_context.rb', line 72

def options
  @options
end

#provided_contextEvaluationContext

Returns A context provided to us that we can use without re-serializing.

Returns:

  • (EvaluationContext)

    A context provided to us that we can use without re-serializing



76
77
78
# File 'lib/json/ld/evaluation_context.rb', line 76

def provided_context
  @provided_context
end

#remote_contextsArray<String>

Returns The list of remote contexts already processed.

Returns:

  • (Array<String>)

    The list of remote contexts already processed



80
81
82
# File 'lib/json/ld/evaluation_context.rb', line 80

def remote_contexts
  @remote_contexts
end

#vocabString

Default vocabulary

Sets the default vocabulary used for expanding terms which aren’t otherwise absolute IRIs

Returns:

  • (String)


68
69
70
# File 'lib/json/ld/evaluation_context.rb', line 68

def vocab
  @vocab
end

Instance Method Details

#alias(value) ⇒ String

Reverse term mapping, typically used for finding aliases for keys.

Returns either the original value, or a mapping for this value.

Examples:

{"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}

Parameters:

  • value (RDF::URI, String)

Returns:

  • (String)


412
413
414
# File 'lib/json/ld/evaluation_context.rb', line 412

def alias(value)
  iri_to_term.fetch(value, value)
end

#coerce(property) ⇒ RDF::URI, '@id'

Retrieve term coercion

Parameters:

  • property (String)

    in unexpanded form

Returns:

  • (RDF::URI, '@id')


422
423
424
425
426
427
# File 'lib/json/ld/evaluation_context.rb', line 422

def coerce(property)
  # Map property, if it's not an RDF::Value
  # @type is always is an IRI
  return '@id' if [RDF.type, '@type'].include?(property)
  @coercions.fetch(property, nil)
end

#compact_iri(iri, options = {}) ⇒ String

Compacts an absolute IRI to the shortest matching term or compact IRI

Parameters:

  • iri (RDF::URI)
  • options (Hash{Symbol => Object}) (defaults to: {})

    ({})

Options Hash (options):

  • position (:subject, :predicate, :type)

    Useful when determining how to serialize.

  • :value (Object)

    Value, used to select among various maps for the same IRI

  • :not_term (Boolean) — default: false

    Don’t return a term, but only a CURIE or IRI.

Returns:

  • (String)

    compacted form of IRI

See Also:



575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
# File 'lib/json/ld/evaluation_context.rb', line 575

def compact_iri(iri, options = {})
  depth(options) do
    debug {"compact_iri(#{iri.inspect}, #{options.inspect})"}

    value = options.fetch(:value, nil)

    # Get a list of terms which map to iri
    matched_terms = mappings.keys.select {|t| mapping(t).to_s == iri}
    debug("compact_iri", "initial terms: #{matched_terms.inspect}")

    # 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.
    terms = {}

    # If value is a @list select terms that match every item equivalently.
    debug("compact_iri", "#{value.inspect} is a list? #{list?(value).inspect}") if value
    if list?(value) && !index?(value)
      list_terms = matched_terms.select {|t| container(t) == '@list'}
        
      terms = list_terms.inject({}) do |memo, t|
        memo[t] = term_rank(t, value)
        memo
      end unless list_terms.empty?
      debug("term map") {"remove zero rank terms: #{terms.keys.select {|t| terms[t] == 0}}"} if terms.any? {|t,r| r == 0}
      terms.delete_if {|t, r| r == 0}
    end
    
    # Otherwise, value is @value or a native type.
    # Add a term rank for each term mapping to iri
    # which does not have @container @list
    if terms.empty?
      non_list_terms = matched_terms.reject {|t| container(t) == '@list'}

      # If value is a @list, exclude from term map those terms
      # with @container @set
      non_list_terms.reject {|t| container(t) == '@set'} if list?(value)

      terms = non_list_terms.inject({}) do |memo, t|
        memo[t] = term_rank(t, value)
        memo
      end unless non_list_terms.empty?
      debug("term map") {"remove zero rank terms: #{terms.keys.select {|t| terms[t] == 0}}"} if terms.any? {|t,r| r == 0}
      terms.delete_if {|t, r| r == 0}
    end

    # If we don't want terms, remove anything that's not a CURIE or IRI
    terms.keep_if {|t, v| t.index(':') } if options.fetch(:not_term, false)

    # Find terms having the greatest term match value
    least_distance = terms.values.max
    terms = terms.keys.select {|t| terms[t] == least_distance}

    # If terms is empty, add a compact IRI representation of iri for each 
    # term in the active context which maps to an IRI which is a prefix for 
    # iri where the resulting compact IRI is not a term in the active 
    # context. The resulting compact IRI is the term associated with the 
    # partially matched IRI in the active context concatenated with a colon 
    # (:) character and the unmatched part of iri.
    if terms.empty?
      debug("curies") {"mappings: #{mappings.inspect}"}
      curies = mappings.keys.map do |k|
        debug("curies[#{k}]") {"#{mapping(k).inspect}"}
        #debug("curies[#{k}]") {"#{(mapping(k).to_s.length > 0).inspect}, #{iri.to_s.index(mapping(k).to_s)}"}
        iri.to_s.sub(mapping(k).to_s, "#{k}:") if
          mapping(k).to_s.length > 0 &&
          iri.to_s.index(mapping(k).to_s) == 0 &&
          iri.to_s != mapping(k).to_s
      end.compact

      debug("curies") do
        curies.map do |c|
          "#{c}: " +
          "container: #{container(c).inspect}, " +
          "coerce: #{coerce(c).inspect}, " +
          "lang: #{language(c).inspect}"
        end.inspect
      end

      terms = curies.select do |curie|
        (options[:position] != :predicate || container(curie) != '@list') &&
        coerce(curie).nil? &&
        language(curie) == default_language
      end

      debug("curies") {"selected #{terms.inspect}"}
    end

    # 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.
    # Don't use vocab, if the result would collide with a term
    if vocab && terms.empty? && iri.to_s.index(vocab) == 0 &&
      !mapping(iri.to_s.sub(vocab, '')) &&
       [:predicate, :type].include?(options[:position])
      terms << iri.to_s.sub(vocab, '')
      debug("vocab") {"vocab: #{vocab}, rel: #{terms.first}"}
    end

    # If we still don't have any terms and we're using standard_prefixes,
    # try those, and add to mapping
    if terms.empty? && @options[:standard_prefixes]
      terms = RDF::Vocabulary.
        select {|v| iri.index(v.to_uri.to_s) == 0}.
        map do |v|
          prefix = v.__name__.to_s.split('::').last.downcase
          set_mapping(prefix, v.to_uri.to_s)
          iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
        end
      debug("curies") {"using standard prefies: #{terms.inspect}"}
    end

    if terms.empty?
      # If there is a mapping from the complete IRI to null, return null,
      # otherwise, return the complete IRI.
      if mappings.has_key?(iri.to_s) && !mapping(iri)
        debug("iri") {"use nil IRI mapping"}
        terms << nil
      else
        terms << iri.to_s
      end
    end

    # Get the first term based on distance and lexecographical order
    # Prefer terms that don't have @container @set over other terms, unless as set is true
    terms = terms.sort do |a, b|
      debug("term sort") {"c(a): #{container(a).inspect}, c(b): #{container(b)}"}
      if a.to_s.length == b.to_s.length
        a.to_s <=> b.to_s
      else
        a.to_s.length <=> b.to_s.length
      end
    end
    debug("sorted terms") {terms.inspect}
    result = terms.first

    debug {"=> #{result.inspect}"}
    result
  end
end

#compact_value(property, value, options = {}) ⇒ Hash

Compact a value

FIXME: revisit the specification version of this.

Parameters:

  • property (String)

    Associated property used to find coercion rules

  • value (Hash)

    Value (literal or IRI), in full object representation, to be compacted

  • options (Hash{Symbol => Object}) (defaults to: {})

Returns:

  • (Hash)

    Object representation of value

Raises:

See Also:



786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
# File 'lib/json/ld/evaluation_context.rb', line 786

def compact_value(property, value, options = {})
  raise ProcessingError::Lossy, "attempt to compact a non-object value: #{value.inspect}" unless value.is_a?(Hash)

  depth(options) do
    debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}

    # Remove @index if property has annotation
    value.delete('@index') if container(property) == '@index'

    result = case
    when value.has_key?('@index')
      # Don't compact the value
      debug {" (@index without container @index)"}
      value
    when coerce(property) == '@id' && value.has_key?('@id')
      # Compact an @id coercion
      debug {" (@id & coerce)"}
      compact_iri(value['@id'], :position => :subject)
    when value['@type'] && expand_iri(value['@type'], :position => :type) == coerce(property)
      # Compact common datatype
      debug {" (@type & coerce) == #{coerce(property)}"}
      value['@value']
    when value.has_key?('@id')
      # Compact an IRI
      value[self.alias('@id')] = compact_iri(value['@id'], :position => :subject)
      debug {" (#{self.alias('@id')} => #{value['@id']})"}
      value
    when value['@language'] && (value['@language'] == language(property) || container(property) == '@language')
      # Compact language
      debug {" (@language) == #{language(property).inspect}"}
      value['@value']
    when !value.fetch('@value', "").is_a?(String)
      # Compact simple literal to string
      debug {" (@value not string)"}
      value['@value']
    when value['@value'] && !value['@language'] && !value['@type'] && !coerce(property) && !default_language
      # Compact simple literal to string
      debug {" (@value && !@language && !@type && !coerce && !language)"}
      value['@value']
    when value['@value'] && !value['@language'] && !value['@type'] && !coerce(property) && !language(property)
      # Compact simple literal to string
      debug {" (@value && !@language && !@type && !coerce && language(property).false)"}
      value['@value']
    when value['@type']
      # Compact datatype
      debug {" (@type)"}
      value[self.alias('@type')] = compact_iri(value['@type'], :position => :type)
      value
    else
      # Otherwise, use original value
      debug {" (no change)"}
      value
    end
    
    # If the result is an object, tranform keys using any term keyword aliases
    if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
      debug {" (map to key aliases)"}
      new_element = {}
      result.each do |k, v|
        new_element[self.alias(k)] = v
      end
      result = new_element
    end

    debug {"=> #{result.inspect}"}
    result
  end
end

#container(property) ⇒ String

Retrieve container mapping, add it if ‘value` is provided

Parameters:

  • property (String)

    in unexpanded form

Returns:

  • (String)


450
451
452
453
# File 'lib/json/ld/evaluation_context.rb', line 450

def container(property)
  return '@set' if property == '@graph'
  @containers.fetch(property.to_s, nil)
end

#dupObject



865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
# File 'lib/json/ld/evaluation_context.rb', line 865

def dup
  # Also duplicate mappings, coerce and list
  that = self
  ec = super
  ec.instance_eval do
    @mappings = that.mappings.dup
    @coerceions = that.coercions.dup
    @containers = that.containers.dup
    @languages = that.languages.dup
    @default_language = that.default_language
    @options = that.options
    @iri_to_term = that.iri_to_term.dup
    @iri_to_curie = that.iri_to_curie.dup
  end
  ec
end

#expand_iri(iri, options = {}) ⇒ RDF::Term, ...

Expand an IRI. Relative IRIs are expanded against any document base.

Parameters:

  • iri (String)

    A keyword, term, prefix:suffix or possibly relative IRI

  • options (Hash{Symbol => Object}) (defaults to: {})

Options Hash (options):

  • position (:subject, :predicate, :type)

    Useful when determining how to serialize.

  • base (RDF::URI) — default: self.base

    Base IRI to use when expanding relative IRIs.

  • path (Array<String>) — default: []

    Array of looked up iris, used to find cycles

  • namer (BlankNodeNamer)

    Blank Node namer to use for renaming Blank Nodes

Returns:

  • (RDF::Term, String, Array<RDF::URI>)

    IRI or String, if it’s a keyword, or array of IRI, if it matches a property generator

Raises:

  • (RDF::ReaderError)

    if the iri cannot be expanded

See Also:



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/json/ld/evaluation_context.rb', line 519

def expand_iri(iri, options = {})
  return iri unless iri.is_a?(String)

  prefix, suffix = iri.split(':', 2)
  unless (m = mapping(iri)) == false
    # It's an exact match
    debug("expand_iri") {"match: #{iri.inspect} to #{m.inspect}"} unless options[:quiet]
    return case m
    when nil
      nil
    when Array
      # Return array of IRIs, if it's a property generator
      m.map {|mm| uri(mm.to_s, options[:namer])}
    else
      uri(m.to_s, options[:namer])
    end
  end
  debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{vocab.inspect}"} unless options[:quiet]
  base = [:subject, :type].include?(options[:position]) ? options.fetch(:base, self.base) : nil
  prefix = prefix.to_s
  case
  when prefix == '_' && suffix          then uri(bnode(suffix), options[:namer])
  when iri.to_s[0,1] == "@"             then iri
  when suffix.to_s[0,2] == '//'         then uri(iri)
  when (mapping = mapping(prefix)) != false
    debug("expand_iri") {"mapping: #{mapping(prefix).inspect}"} unless options[:quiet]
    case mapping
    when Array
      # Return array of IRIs, if it's a property generator
      mapping.map {|m| uri(m.to_s + suffix.to_s, options[:namer])}
    else
      uri(mapping.to_s + suffix.to_s, options[:namer])
    end
  when base                             then base.join(iri)
  when vocab                            then uri("#{vocab}#{iri}")
  else
    # Otherwise, it must be an absolute IRI
    u = uri(iri)
    u if u.absolute? || [:subject, :type].include?(options[:position])
  end
end

#expand_value(property, value, options = {}) ⇒ Hash

Expand a value from compacted to expanded form making the context unnecessary. This method is used as part of more general expansion and operates on RHS values, using a supplied key to determine @type and

Parameters:

  • property (String)

    Associated property used to find coercion rules

  • value (Hash, String)

    Value (literal or IRI) to be expanded

  • options (Hash{Symbol => Object}) (defaults to: {})

Options Hash (options):

  • :useNativeTypes (Boolean) — default: true

    use native representations

  • namer (BlankNodeNamer)

    Blank Node namer to use for renaming Blank Nodes

Returns:

  • (Hash)

    Object representation of value

Raises:

  • (RDF::ReaderError)

    if the iri cannot be expanded

See Also:



730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
# File 'lib/json/ld/evaluation_context.rb', line 730

def expand_value(property, value, options = {})
  options = {:useNativeTypes => true}.merge(options)
  depth(options) do
    debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}

    value = if value.is_a?(RDF::Value)
      value
    elsif coerce(property) == '@id'
      expand_iri(value, :position => :subject, :namer => options[:namer])
    else
      RDF::Literal(value)
    end
    debug("expand_value") {"normalized: #{value.inspect}"}
    
    result = case value
    when RDF::URI, RDF::Node
      debug("URI | BNode") { value.to_s }
      {'@id' => value.to_s}
    when RDF::Literal
      debug("Literal") {"datatype: #{value.datatype.inspect}"}
      res = Hash.ordered
      if options[:useNativeTypes] && [RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double].include?(value.datatype)
        res['@value'] = value.object
        res['@type'] = uri(coerce(property), options[:namer]) if coerce(property)
      else
        value.canonicalize! if value.datatype == RDF::XSD.double
        res['@value'] = value.to_s
        if coerce(property)
          res['@type'] = uri(coerce(property), options[:namer]).to_s
        elsif value.has_datatype?
          res['@type'] = uri(value.datatype, options[:namer]).to_s
        elsif value.has_language? || language(property)
          res['@language'] = (value.language || language(property)).to_s
        end
      end
      res
    end

    debug {"=> #{result.inspect}"}
    result
  end
end

#inspectObject



855
856
857
858
859
860
861
862
863
# File 'lib/json/ld/evaluation_context.rb', line 855

def inspect
  v = %w([EvaluationContext)
  v << "def_language=#{default_language}"
  v << "languages[#{languages.keys.length}]=#{languages}"
  v << "mappings[#{mappings.keys.length}]=#{mappings}"
  v << "coercions[#{coercions.keys.length}]=#{coercions}"
  v << "containers[#{containers.length}]=#{containers}"
  v.join(", ") + "]"
end

#language(property) ⇒ String

Retrieve the language associated with a property, or the default language otherwise

Returns:

  • (String)


474
475
476
# File 'lib/json/ld/evaluation_context.rb', line 474

def language(property)
  @languages.fetch(property.to_s, @default_language) if !coerce(property)
end

#mapping(term) ⇒ RDF::URI, String

Retrieve term mapping

Parameters:

  • term (String, #to_s)

Returns:

  • (RDF::URI, String)


379
380
381
# File 'lib/json/ld/evaluation_context.rb', line 379

def mapping(term)
  @mappings.fetch(term.to_s, false)
end

#parse(context) ⇒ Object

Create an Evaluation Context using an existing context as a start by parsing the input.

Parameters:

  • context (String, #read, Array, Hash, EvaluatoinContext)

Raises:

  • (InvalidContext)

    on a remote context load error, syntax error, or a reference to a term which is not defined.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/json/ld/evaluation_context.rb', line 117

def parse(context)
  case context
  when EvaluationContext
    debug("parse") {"context: #{context.inspect}"}
    context.dup
  when IO, StringIO
    debug("parse") {"io: #{context}"}
    # Load context document, if it is a string
    begin
      ctx = JSON.load(context)
      raise JSON::LD::InvalidContext::LoadError, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
      parse(ctx["@context"] || {})
    rescue JSON::ParserError => e
      debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
      raise JSON::LD::InvalidContext::Syntax, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
      self.dup
    end
  when nil
    debug("parse") {"nil"}
    # Load context document, if it is a string
    ec = EvaluationContext.new(options)
  when String
    debug("parse") {"remote: #{context}, base: #{context_base || base}"}
    # Load context document, if it is a string
    ec = nil
    begin
      url = expand_iri(context, :base => context_base || base, :position => :subject)
      raise JSON::LD::InvalidContext::LoadError if remote_contexts.include?(url)
      @remote_contexts = @remote_contexts + [url]
      ecdup = self.dup
      ecdup.context_base = url  # Set context_base for recursive remote contexts
      RDF::Util::File.open_file(url) {|f| ec = ecdup.parse(f)}
      ec.provided_context = context
      ec.context_base = url
      debug("parse") {"=> provided_context: #{context.inspect}"}
      ec
    rescue Exception => e
      debug("parse") {"Failed to retrieve @context from remote document at #{context.inspect}: #{e.message}"}
      raise JSON::LD::InvalidContext::LoadError, "Failed to retrieve remote context at #{context.inspect}: #{e.message}", e.backtrace if @options[:validate]
      self.dup
    end
  when Array
    # Process each member of the array in order, updating the active context
    # Updates evaluation context serially during parsing
    debug("parse") {"Array"}
    ec = self
    context.each {|c| ec = ec.parse(c)}
    ec.provided_context = context
    debug("parse") {"=> provided_context: #{context.inspect}"}
    ec
  when Hash
    new_ec = self.dup
    new_ec.provided_context = context.dup

    # If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
    {
      '@language' => :default_language=,
      '@vocab'    => :vocab=
    }.each do |key, setter|
      v = context.fetch(key, false)
      if v.nil? || v.is_a?(String)
        context.delete(key)
        debug("parse") {"Set #{key} to #{v.inspect}"}
        new_ec.send(setter, v)
      elsif v && @options[:validate]
        raise InvalidContext::Syntax, "#{key.inspect} is invalid"
      end
    end

    num_updates = 1
    while num_updates > 0 do
      num_updates = 0

      # Map terms to IRIs/keywords first
      context.each do |key, value|
        # Expand a string value, unless it matches a keyword
        debug("parse") {"Hash[#{key}] = #{value.inspect}"}

        if KEYWORDS.include?(key)
          raise InvalidContext::Syntax, "key #{key.inspect} must not be a keyword" if @options[:validate]
          next
        elsif term_valid?(key)
          # Remove all coercion information for the property
          new_ec.set_coerce(key, nil)
          new_ec.set_container(key, nil)
          @languages.delete(key)

          # Extract IRI mapping. This is complicated, as @id may have been aliased. Also, if @id is explicitly set to nil, it inhibits and automatic mapping, so treat it as false, to distinguish from no mapping at all.
          value = case value
          when Hash
            value.has_key?('@id') && value['@id'].nil? ? false : value.fetch('@id', nil)
          when nil
            false
          else
            value
          end

          # Explicitly say this is not mapped
          if value == false
            debug("parse") {"Map #{key} to nil"}
            new_ec.set_mapping(key, nil)
            next
          end

          iri = if value.is_a?(Array)
            # expand each item according the IRI Expansion algorithm. If an item does not expand to a valid absolute IRI, raise an INVALID_PROPERTY_GENERATOR error; otherwise sort val and store it as IRI mapping in definition.
            value.map do |v|
              raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{v.inspect}" unless v.is_a?(String)
              new_ec.expand_iri(v, :position => :predicate)
            end.sort
          elsif value
            raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{value.inspect}" unless value.is_a?(String)
            new_ec.expand_iri(value, :position => :predicate)
          end

          if iri && new_ec.mappings.fetch(key, nil) != iri
            # Record term definition
            new_ec.set_mapping(key, iri)
            num_updates += 1
          end
        elsif @options[:validate]
          raise InvalidContext::Syntax, "key #{key.inspect} is invalid"
        end
      end
    end

    # Next, look for coercion using new_ec
    context.each do |key, value|
      # Expand a string value, unless it matches a keyword
      debug("parse") {"coercion/list: Hash[#{key}] = #{value.inspect}"}
      case value
      when Hash
        # Must have one of @id, @language, @type or @container
        raise InvalidContext::Syntax, "mapping for #{key.inspect} missing one of @id, @language, @type or @container" if (%w(@id @language @type @container) & value.keys).empty?
        value.each do |key2, value2|
          iri = new_ec.expand_iri(value2, :position => :predicate) if value2.is_a?(String)
          case key2
          when '@type'
            raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.inspect}" unless value2.is_a?(String) || value2.nil?
            if new_ec.coerce(key) != iri
              case iri
              when '@id', /_:/, RDF::Node
              else
                raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless (RDF::URI(iri).absolute? rescue false)
              end
              # Record term coercion
              new_ec.set_coerce(key, iri)
            end
          when '@container'
            raise InvalidContext::Syntax, "unknown mapping for '@container' to #{value2.inspect}" unless %w(@list @set @language @index).include?(value2)
            if new_ec.container(key) != value2
              debug("parse") {"container #{key.inspect} as #{value2.inspect}"}
              new_ec.set_container(key, value2)
            end
          when '@language'
            if !new_ec.languages.has_key?(key) || new_ec.languages[key] != value2
              debug("parse") {"language #{key.inspect} as #{value2.inspect}"}
              new_ec.set_language(key, value2)
            end
          end
        end
      
        # If value has no @id, create a mapping from key
        # to the expanded key IRI
        unless value.has_key?('@id')
          iri = new_ec.expand_iri(key, :position => :predicate)
          new_ec.set_mapping(key, iri)
        end
      when nil, String
        # handled in previous loop
      else
        raise InvalidContext::Syntax, "attempt to map #{key.inspect} to #{value.class}"
      end
    end

    new_ec
  end
end

#serialize(options = {}) ⇒ Hash

Generate @context

If a context was supplied in global options, use that, otherwise, generate one from this representation.

Parameters:

  • options (Hash{Symbol => Object}) (defaults to: {})

    ({})

Returns:



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/json/ld/evaluation_context.rb', line 304

def serialize(options = {})
  depth(options) do
    use_context = if provided_context
      debug "serlialize: reuse context: #{provided_context.inspect}"
      provided_context
    else
      debug("serlialize: generate context")
      debug {"=> context: #{inspect}"}
      ctx = Hash.ordered
      ctx['@language'] = default_language.to_s if default_language
      ctx['@vocab'] = vocab.to_s if vocab

      # Mappings
      mappings.keys.kw_sort{|a, b| a.to_s <=> b.to_s}.each do |k|
        next unless term_valid?(k.to_s)
        debug {"=> mappings[#{k}] => #{mappings[k]}"}
        ctx[k] = mappings[k]
      end

      unless coercions.empty? && containers.empty? && languages.empty?
        # Coerce
        (coercions.keys + containers.keys + languages.keys).uniq.sort.each do |k|
          next if k == '@type'

          # Turn into long form
          ctx[k] ||= Hash.ordered
          if ctx[k].is_a?(String)
            defn = Hash.ordered
            defn["@id"] = compact_iri(ctx[k], :position => :subject, :not_term => true)
            ctx[k] = defn
          end

          debug {"=> coerce(#{k}) => #{coerce(k)}"}
          if coerce(k) && !NATIVE_DATATYPES.include?(coerce(k))
            dt = coerce(k)
            dt = compact_iri(dt, :position => :type) unless dt == '@id'
            # Fold into existing definition
            ctx[k]["@type"] = dt
            debug {"=> datatype[#{k}] => #{dt}"}
          end

          debug {"=> container(#{k}) => #{container(k)}"}
          if %w(@list @set @language @index).include?(container(k))
            ctx[k]["@container"] = container(k)
            debug {"=> container[#{k}] => #{container(k).inspect}"}
          end

          debug {"=> language(#{k}) => #{language(k)}"}
          if language(k) != default_language
            ctx[k]["@language"] = language(k) ? language(k) : nil
            debug {"=> language[#{k}] => #{language(k).inspect}"}
          end
          
          # Remove an empty definition
          ctx.delete(k) if ctx[k].empty?
        end
      end

      debug {"start_doc: context=#{ctx.inspect}"}
      ctx
    end

    # Return hash with @context, or empty
    r = Hash.ordered
    r['@context'] = use_context unless use_context.nil? || use_context.empty?
    r
  end
end

#set_coerce(property, value) ⇒ RDF::URI, '@id'

Set term coercion

Parameters:

  • property (String)

    in unexpanded form

  • value (RDF::URI, '@id')

Returns:

  • (RDF::URI, '@id')


436
437
438
439
440
441
442
443
# File 'lib/json/ld/evaluation_context.rb', line 436

def set_coerce(property, value)
  debug {"coerce #{property.inspect} to #{value.inspect}"} unless @coercions[property.to_s] == value
  if value
    @coercions[property] = value
  else
    @coercions.delete(property)
  end
end

#set_container(property, value) ⇒ Boolean

Set container mapping

Parameters:

  • property (String)
  • value (String)

    one of @list, @set or nil

Returns:

  • (Boolean)


461
462
463
464
465
466
467
468
469
# File 'lib/json/ld/evaluation_context.rb', line 461

def set_container(property, value)
  return if @containers[property.to_s] == value
  debug {"coerce #{property.inspect} to #{value.inspect}"} 
  if value
    @containers[property.to_s] = value
  else
    @containers.delete(value)
  end
end

#set_language(property, value) ⇒ String

Set language mapping

Parameters:

  • property (String)
  • value (String)

Returns:

  • (String)


484
485
486
487
# File 'lib/json/ld/evaluation_context.rb', line 484

def set_language(property, value)
  # Use false for nil language
  @languages[property.to_s] = value ? value : false
end

#set_mapping(term, value) ⇒ RDF::URI, String

Set term mapping

Parameters:

  • term (#to_s)
  • value (RDF::URI, String, nil)

Returns:

  • (RDF::URI, String)


390
391
392
393
394
395
396
397
398
399
400
# File 'lib/json/ld/evaluation_context.rb', line 390

def set_mapping(term, value)
  term = term.to_s
  term_sym = term.empty? ? "" : term.to_sym
#      raise InvalidContext::Syntax, "mapping term #{term.inspect} must be a string" unless term.is_a?(String)
#      raise InvalidContext::Syntax, "mapping value #{value.inspect} must be an RDF::URI" unless value.nil? || value.to_s[0,1] == '@' || value.is_a?(RDF::URI)
  debug {"map #{term.inspect} to #{value.inspect}"}
  iri_to_term.delete(@mappings[term].to_s) if @mappings[term]
  @mappings[term] = value
  @options[:prefixes][term_sym] = value if @options.has_key?(:prefixes)
  iri_to_term[value.to_s] = term
end

#term_valid?(term) ⇒ Boolean

Determine if ‘term` is a suitable term. Term may be any valid JSON string.

Parameters:

  • term (String)

Returns:

  • (Boolean)


495
496
497
# File 'lib/json/ld/evaluation_context.rb', line 495

def term_valid?(term)
  term.is_a?(String)
end