Class: JSON::LD::Context

Inherits:
Object
  • Object
show all
Includes:
Utils, RDF::Util::Logger
Defined in:
lib/json/ld/context.rb

Defined Under Namespace

Classes: TermDefinition

Constant Summary collapse

PRELOADED =

Preloaded contexts. To avoid runtime context parsing and downloading, contexts may be pre-loaded by implementations.

Returns:

{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

#add_value, #as_resource, #blank_node?, #compare_values, #has_property, #has_value, #index?, #list?, #node?, #node_reference?, #value?

Constructor Details

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

Create new evaluation context

Parameters:

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :base (String, #to_s)

    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.

  • :documentLoader (Proc)

    The callback of the loader to be used to retrieve remote documents and contexts. If specified, it must be used to retrieve remote documents and contexts; otherwise, if not specified, the processor’s built-in loader must be used. See API.documentLoader for the method signature.

  • :prefixes (Hash{Symbol => String})

    See ‘RDF::Reader#initialize`

  • :simple_compact_iris (Boolean) — default: false

    When compacting IRIs, do not use terms with expanded term definitions

  • :vocab (String, #to_s)

    Initial value for @vocab

  • :language (String, #to_s)

    Initial value for @langauge

Yields:

  • (ec)

Yield Parameters:



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
# File 'lib/json/ld/context.rb', line 207

def initialize(options = {})
  if options[:base]
    @base = @doc_base = RDF::URI(options[:base]).dup
    @doc_base.canonicalize! if options[:canonicalize]
  end
  options[:documentLoader] ||= JSON::LD::API.method(:documentLoader)
  @term_definitions = {}
  @iri_to_term = {
    RDF.to_uri.to_s => "rdf",
    RDF::XSD.to_uri.to_s => "xsd"
  }
  @namer = BlankNodeMapper.new("t")

  @options = options

  # Load any defined prefixes
  (options[:prefixes] || {}).each_pair do |k, v|
    next if k.nil?
    @iri_to_term[v.to_s] = k
    @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s)
    @term_definitions[k.to_s].simple = true
  end

  self.vocab = options[:vocab] if options[:vocab]
  self.default_language = options[:language] if options[:language]
  @term_definitions = options[:term_definitions] if options[:term_definitions]

  #log_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)

    Current base IRI, used for expanding relative IRIs.



149
150
151
# File 'lib/json/ld/context.rb', line 149

def base
  @base
end

#context_baseRDF::URI

Returns base IRI of the context, if loaded remotely. XXX.

Returns:

  • (RDF::URI)

    base IRI of the context, if loaded remotely. XXX



157
158
159
# File 'lib/json/ld/context.rb', line 157

def context_base
  @context_base
end

#default_languageString

Default language

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

Returns:

  • (String)


171
172
173
# File 'lib/json/ld/context.rb', line 171

def default_language
  @default_language
end

#doc_baseRDF::URI (readonly)

The base.

Returns:

  • (RDF::URI)

    Document base IRI, to initialize ‘base`.



154
155
156
# File 'lib/json/ld/context.rb', line 154

def doc_base
  @doc_base
end

#iri_to_termHash{RDF::URI => String}

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

Returns:

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

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



164
165
166
# File 'lib/json/ld/context.rb', line 164

def iri_to_term
  @iri_to_term
end

#namerBlankNodeNamer

Returns:



187
188
189
# File 'lib/json/ld/context.rb', line 187

def namer
  @namer
end

#optionsHash{Symbol => Object}

Returns Global options used in generating IRIs.

Returns:

  • (Hash{Symbol => Object})

    Global options used in generating IRIs



181
182
183
# File 'lib/json/ld/context.rb', line 181

def options
  @options
end

#provided_contextContext

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

Returns:

  • (Context)

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



184
185
186
# File 'lib/json/ld/context.rb', line 184

def provided_context
  @provided_context
end

#term_definitionsHash{String => TermDefinition} (readonly)

Term definitions

Returns:



161
162
163
# File 'lib/json/ld/context.rb', line 161

def term_definitions
  @term_definitions
end

#vocabRDF::URI

Default vocabulary

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

Returns:

  • (RDF::URI)


178
179
180
# File 'lib/json/ld/context.rb', line 178

def vocab
  @vocab
end

Class Method Details

.add_preloaded(url, context = nil, &block) ⇒ Object

Add preloaded context. In the block form, the context is lazy evaulated on first use.

Parameters:

  • url (String, RDF::URI)
  • context (Context) (defaults to: nil)

    (nil)

Yield Returns:



23
24
25
# File 'lib/json/ld/context.rb', line 23

def add_preloaded(url, context = nil, &block)
  PRELOADED[url.to_s.freeze] = context || block
end

Instance Method Details

#compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **options) ⇒ String

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

Parameters:

  • iri (RDF::URI)
  • value (Object) (defaults to: nil)

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

  • :vocab (Boolean)

    specifies whether the passed iri should be compacted using the active context’s vocabulary mapping

  • reverse (Boolean) (defaults to: false)

    specifies whether a reverse property is being compacted

  • quiet (Boolean) (defaults to: false)

    (false)

  • options (Hash{Symbol => Object})

    ({})

Returns:

  • (String)

    compacted form of IRI

See Also:



890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
# File 'lib/json/ld/context.rb', line 890

def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **options)
  return if iri.nil?
  iri = iri.to_s
  #log_debug("compact_iri(#{iri.inspect}", options) {[value, vocab, reverse].inspect} unless quiet

  if vocab && inverse_context.has_key?(iri)
    #log_debug("") {"vocab and key in inverse context"} unless quiet
    default_language = self.default_language || @none
    containers = []
    tl, tl_value = "@language", "@null"
    containers << '@index' if index?(value)
    if reverse
      tl, tl_value = "@type", "@reverse"
      containers << '@set'
    elsif list?(value)
      #log_debug("") {"list(#{value.inspect})"} unless quiet
      # if value is a list object, then set type/language and type/language value to the most specific values that work for all items in the list as follows:
      containers << "@list" unless index?(value)
      list = value['@list']
      common_type = nil
      common_language = default_language if list.empty?
      list.each do |item|
        item_language, item_type = "@none", "@none"
        if value?(item)
          if item.has_key?('@language')
            item_language = item['@language']
          elsif item.has_key?('@type')
            item_type = item['@type']
          else
            item_language = "@null"
          end
        else
          item_type = '@id'
        end
        common_language ||= item_language
        if item_language != common_language && value?(item)
          #log_debug("") {"-- #{item_language} conflicts with #{common_language}, use @none"} unless quiet
          common_language = '@none'
        end
        common_type ||= item_type
        if item_type != common_type
          common_type = '@none'
          #log_debug("") {"#{item_type} conflicts with #{common_type}, use @none"} unless quiet
        end
      end

      common_language ||= '@none'
      common_type ||= '@none'
      #log_debug("") {"common type: #{common_type}, common language: #{common_language}"} unless quiet
      if common_type != '@none'
        tl, tl_value = '@type', common_type
      else
        tl_value = common_language
      end
      #log_debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
    else
      if value?(value)
        if value.has_key?('@language') && !index?(value)
          tl_value = value['@language']
          containers << '@language'
        elsif value.has_key?('@type')
          tl_value = value['@type']
          tl = '@type'
        end
      else
        tl, tl_value = '@type', '@id'
      end
      containers << '@set'
      #log_debug("") {"value: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
    end

    containers << '@none'
    tl_value ||= '@null'
    preferred_values = []
    preferred_values << '@reverse' if tl_value == '@reverse'
    if %w(@id @reverse).include?(tl_value) && value.is_a?(Hash) && value.has_key?('@id')
      t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
      if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
        preferred_values.concat(%w(@vocab @id @none))
      else
        preferred_values.concat(%w(@id @vocab @none))
      end
    else
      preferred_values.concat([tl_value, '@none'])
    end
    #log_debug("") {"preferred_values: #{preferred_values.inspect}"} unless quiet
    if p_term = select_term(iri, containers, tl, preferred_values)
      #log_debug("") {"=> term: #{p_term.inspect}"} unless quiet
      return p_term
    end
  end

  # At this point, there is no simple term that iri can be compacted to. If vocab is true and active context has a vocabulary mapping:
  if vocab && self.vocab && iri.start_with?(self.vocab) && iri.length > self.vocab.length
    suffix = iri[self.vocab.length..-1]
    #log_debug("") {"=> vocab suffix: #{suffix.inspect}"} unless quiet
    return suffix unless term_definitions.has_key?(suffix)
  end

  # The iri could not be compacted using the active context's vocabulary mapping. Try to create a compact IRI, starting by initializing compact IRI to null. This variable will be used to tore the created compact IRI, if any.
  candidates = []

  term_definitions.each do |term, td|
    next if term.include?(":")
    next if td.nil? || td.id.nil? || td.id == iri || !iri.start_with?(td.id)

    # Also skip term if it was not a simple term and the :simple_compact_iris flag is true
    next if @options[:simple_compact_iris] && !td.simple?

    suffix = iri[td.id.length..-1]
    ciri = "#{term}:#{suffix}"
    candidates << ciri unless value && term_definitions.has_key?(ciri)
  end

  if !candidates.empty?
    #log_debug("") {"=> compact iri: #{candidates.term_sort.first.inspect}"} unless quiet
    return candidates.term_sort.first
  end

  # If we still don't have any terms and we're using standard_prefixes,
  # try those, and add to mapping
  if @options[:standard_prefixes]
    candidates = RDF::Vocabulary.
      select {|v| iri.start_with?(v.to_uri.to_s) && iri != v.to_uri.to_s}.
      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

    if !candidates.empty?
      #log_debug("") {"=> standard prefies: #{candidates.term_sort.first.inspect}"} unless quiet
      return candidates.term_sort.first
    end
  end

  if !vocab
    # transform iri to a relative IRI using the document's base IRI
    iri = remove_base(iri)
    #log_debug("") {"=> relative iri: #{iri.inspect}"} unless quiet
    return iri
  else
    #log_debug("") {"=> absolute iri: #{iri.inspect}"} unless quiet
    return iri
  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:



1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
# File 'lib/json/ld/context.rb', line 1127

def compact_value(property, value, options = {})
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}

  num_members = value.keys.length

  num_members -= 1 if index?(value) && container(property) == '@index'
  if num_members > 2
    #log_debug("") {"can't compact value with # members > 2"}
    return value
  end

  result = case
  when coerce(property) == '@id' && value.has_key?('@id') && num_members == 1
    # Compact an @id coercion
    #log_debug("") {" (@id & coerce)"}
    compact_iri(value['@id'])
  when coerce(property) == '@vocab' && value.has_key?('@id') && num_members == 1
    # Compact an @id coercion
    #log_debug("") {" (@id & coerce & vocab)"}
    compact_iri(value['@id'], vocab: true)
  when value.has_key?('@id')
    #log_debug("") {" (@id)"}
    # return value as is
    value
  when value['@type'] && expand_iri(value['@type'], vocab: true) == coerce(property)
    # Compact common datatype
    #log_debug("") {" (@type & coerce) == #{coerce(property)}"}
    value['@value']
  when value['@language'] && (value['@language'] == language(property))
    # Compact language
    #log_debug("") {" (@language) == #{language(property).inspect}"}
    value['@value']
  when num_members == 1 && !value['@value'].is_a?(String)
    #log_debug("") {" (native)"}
    value['@value']
  when num_members == 1 && default_language.nil? || language(property) == false
    #log_debug("") {" (!@language)"}
    value['@value']
  else
    # Otherwise, use original value
    #log_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}
    #log_debug("") {" (map to key aliases)"}
    new_element = {}
    result.each do |k, v|
      new_element[self.alias(k)] = v
    end
    result = new_element
  end

  #log_debug("") {"=> #{result.inspect}"}
  result
end

#container(term) ⇒ String

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

Parameters:

  • term (Term, #to_s)

    in unexpanded form

Returns:

  • (String)


748
749
750
751
752
753
# File 'lib/json/ld/context.rb', line 748

def container(term)
  return '@set' if term == '@graph'
  return term if KEYWORDS.include?(term)
  term = find_definition(term)
  term && term.container_mapping
end

#create_term_definition(local_context, term, defined) ⇒ Object

Create Term Definition

Term definitions are created by parsing the information in the given local context for the given term. If the given term is a compact IRI, it may omit an IRI mapping by depending on its prefix having its own term definition. If the prefix is a key in the local context, then its term definition must first be created, through recursion, before continuing. Because a term definition can depend on other term definitions, a mechanism must be used to detect cyclical dependencies. The solution employed here uses a map, defined, that keeps track of whether or not a term has been defined or is currently in the process of being defined. This map is checked before any recursion is attempted.

After all dependencies for a term have been defined, the rest of the information in the local context for the given term is taken into account, creating the appropriate IRI mapping, container mapping, and type mapping or language mapping for the term.

Parameters:

  • local_context (Hash)
  • term (String)
  • defined (Hash)

Raises:

  • (JsonLdError)

    Represents a cyclical term dependency

See Also:



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
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
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/json/ld/context.rb', line 459

def create_term_definition(local_context, term, defined)
  # Expand a string value, unless it matches a keyword
  #log_debug("create_term_definition") {"term = #{term.inspect}"}

  # If defined contains the key term, then the associated value must be true, indicating that the term definition has already been created, so return. Otherwise, a cyclical term definition has been detected, which is an error.
  case defined[term]
  when TrueClass then return
  when nil
    defined[term] = false
  else
    raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
  end

  # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
  if KEYWORDS.include?(term) && !%w(@vocab @language).include?(term)
    raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
      @options[:validate]
  elsif !term_valid?(term) && @options[:validate]
    raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
  end

  # Remove any existing term definition for term in active context.
  term_definitions.delete(term)

  # Initialize value to a the value associated with the key term in local context.
  value = local_context.fetch(term, false)
  simple_term = value.is_a?(String)
  value = {'@id' => value} if value.is_a?(String)

  case value
  when nil, {'@id' => nil}
    # If value equals null or value is a JSON object containing the key-value pair (@id-null), then set the term definition in active context to null, set the value associated with defined's key term to true, and return.
    #log_debug("") {"=> nil"}
    term_definitions[term] = TermDefinition.new(term)
    defined[term] = true
    return
  when Hash
    #log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
    definition = TermDefinition.new(term)
    definition.simple = simple_term

    if value.has_key?('@type')
      type = value['@type']
      # SPEC FIXME: @type may be nil
      type = case type
      when nil
        type
      when String
        begin
          expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
        rescue JsonLdError::InvalidIRIMapping
          raise JsonLdError::InvalidTypeMapping, "invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
        end
      else
        :error
      end
      unless %w(@id @vocab).include?(type) || type.is_a?(RDF::URI) && type.absolute?
        raise JsonLdError::InvalidTypeMapping, "unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
      end
      #log_debug("") {"type_mapping: #{type.inspect}"}
      definition.type_mapping = type
    end

    if value.has_key?('@reverse')
      raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
        value.keys.any? {|k| %w(@id).include?(k)}
      raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
        value['@reverse'].is_a?(String)

      # Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm, passing active context, the value associated with the @reverse key for value, true for vocab, true for document relative, local context, and defined. If the result is not an absolute IRI, i.e., it contains no colon (:), an invalid IRI mapping error has been detected and processing is aborted.
      definition.id =  expand_iri(value['@reverse'],
                                  vocab: true,
                                  documentRelative: true,
                                  local_context: local_context,
                                  defined: defined)
      raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
        definition.id.is_a?(RDF::URI) && definition.id.absolute?

      # If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, nor @index, nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
      if (container = value.fetch('@container', false))
        raise JsonLdError::InvalidReverseProperty,
              "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
               ['@set', '@index', nil].include?(container)
        definition.container_mapping = container
      end
      definition.reverse_property = true
    elsif value.has_key?('@id') && value['@id'] != term
      raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
        value['@id'].is_a?(String)
      definition.id = expand_iri(value['@id'],
        vocab: true,
        documentRelative: true,
        local_context: local_context,
        defined: defined)
      raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
        definition.id == '@context'
    elsif term.include?(':')
      # If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
      prefix, suffix = term.split(':')
      create_term_definition(local_context, prefix, defined) if local_context.has_key?(prefix)

      definition.id = if td = term_definitions[prefix]
        # If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix.
        td.id + suffix
      else
        # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
        term
      end
      #log_debug("") {"=> #{definition.id}"}
    else
      # Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term.
      raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
      definition.id = vocab + term
      #log_debug("") {"=> #{definition.id}"}
    end

    @iri_to_term[definition.id] = term if simple_term && definition.id

    if value.has_key?('@container')
      container = value['@container']
      raise JsonLdError::InvalidContainerMapping, "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless %w(@list @set @language @index).include?(container)
      #log_debug("") {"container_mapping: #{container.inspect}"}
      definition.container_mapping = container
    end

    if value.has_key?('@language')
      language = value['@language']
      raise JsonLdError::InvalidLanguageMapping, "language must be null or a string, was #{language.inspect}} on term #{term.inspect}" unless language.nil? || (language || "").is_a?(String)
      language = language.downcase if language.is_a?(String)
      #log_debug("") {"language_mapping: #{language.inspect}"}
      definition.language_mapping = language || false
    end

    term_definitions[term] = definition
    defined[term] = true
  else
    raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}"
  end
end

#dupObject



1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
# File 'lib/json/ld/context.rb', line 1216

def dup
  # Also duplicate mappings, coerce and list
  that = self
  ec = super
  ec.instance_eval do
    @term_definitions = that.term_definitions.dup
    @iri_to_term = that.iri_to_term.dup
  end
  ec
end

#empty?Boolean

Initial context, without mappings, vocab or default language

Returns:

  • (Boolean)


243
244
245
# File 'lib/json/ld/context.rb', line 243

def empty?
  @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
end

#expand_iri(value, documentRelative: false, vocab: false, local_context: nil, defined: {}, quiet: false, **options) ⇒ RDF::URI, String

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

Parameters:

  • value (String)

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

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

    (false)

  • vocab (Boolean) (defaults to: false)

    (false)

  • base (RDF::URI)
  • local_context (Hash) (defaults to: nil)

    Used during Context Processing.

  • defined (Hash) (defaults to: {})

    Used during Context Processing.

  • quiet (Boolean) (defaults to: false)

    (false)

  • options (Hash)

    ({})

Returns:

  • (RDF::URI, String)

    IRI or String, if it’s a keyword

Raises:

See Also:



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
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
# File 'lib/json/ld/context.rb', line 814

def expand_iri(value, documentRelative: false, vocab: false, local_context: nil, defined: {}, quiet: false, **options)
  return value unless value.is_a?(String)

  return value if KEYWORDS.include?(value)
  #log_debug("expand_iri") {"value: #{value.inspect}"} unless quiet

  # If local context is not null, it contains a key that equals value, and the value associated with the key that equals value in defined is not true, then invoke the Create Term Definition subalgorithm, passing active context, local context, value as term, and defined. This will ensure that a term definition is created for value in active context during Context Processing.
  if local_context && local_context.has_key?(value) && !defined[value]
    create_term_definition(local_context, value, defined)
  end

  # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
  if vocab && (v_td = term_definitions[value])
    #log_debug("") {"match with #{v_td.id}"} unless quiet
    return v_td.id
  end

  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
  if value.include?(':')
    prefix, suffix = value.split(':', 2)
    #log_debug("") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{self.vocab.inspect}"} unless quiet

    # If prefix is underscore (_) or suffix begins with double-forward-slash (//), return value as it is already an absolute IRI or a blank node identifier.
    return RDF::Node.new(namer.get_sym(suffix)) if prefix == '_'
    return RDF::URI(value) if suffix[0,2] == '//'

    # If local context is not null, it contains a key that equals prefix, and the value associated with the key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active context, local context, prefix as term, and defined. This will ensure that a term definition is created for prefix in active context during Context Processing.
    if local_context && local_context.has_key?(prefix) && !defined[prefix]
      create_term_definition(local_context, prefix, defined)
    end

    # If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
    result = if (td = term_definitions[prefix])
      result = td.id + suffix
    else
      # (Otherwise) Return value as it is already an absolute IRI.
      RDF::URI(value)
    end

    #log_debug("") {"=> #{result.inspect}"} unless quiet
    return result
  end
  #log_debug("") {"=> #{result.inspect}"} unless quiet

  result = if vocab && self.vocab
    # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
    self.vocab + value
  elsif documentRelative && (base ||= self.base)
    # Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
    value = RDF::URI(value)
    value.absolute? ? value : RDF::URI(base).join(value)
  elsif local_context && RDF::URI(value).relative?
    # If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
    raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
  else
    RDF::URI(value)
  end
  #log_debug("") {"=> #{result}"} unless quiet
  result
end

#expand_value(property, value, useNativeTypes: false, **options) ⇒ Hash

If active property has a type mapping in the active context set to @id or @vocab, a JSON object with a single member @id whose value is the result of using the IRI Expansion algorithm on value is returned.

Otherwise, the result will be a JSON object containing an @value member whose value is the passed value. Additionally, an @type member will be included if there is a type mapping associated with the active property or an @language member if value is a string and there is language mapping associated with the active property.

Parameters:

  • property (String)

    Associated property used to find coercion rules

  • value (Hash, String)

    Value (literal or IRI) to be expanded

  • useNativeTypes (Boolean) (defaults to: false)

    (false) use native representations

  • options (Hash{Symbol => Object})

Returns:

  • (Hash)

    Object representation of value

Raises:

  • (RDF::ReaderError)

    if the iri cannot be expanded

See Also:



1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
# File 'lib/json/ld/context.rb', line 1052

def expand_value(property, value, useNativeTypes: false, **options)
  #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}

  # If the active property has a type mapping in active context that is @id, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
  if (td = term_definitions.fetch(property, TermDefinition.new(property))) && td.type_mapping == '@id'
    #log_debug("") {"as relative IRI: #{value.inspect}"}
    return {'@id' => expand_iri(value, documentRelative: true).to_s}
  end

  # If active property has a type mapping in active context that is @vocab, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, true for vocab, and true for document relative.
  if td.type_mapping == '@vocab'
    #log_debug("") {"as vocab IRI: #{value.inspect}"}
    return {'@id' => expand_iri(value, vocab: true, documentRelative: true).to_s}
  end

  value = RDF::Literal(value) if
    value.is_a?(Date) ||
    value.is_a?(DateTime) ||
    value.is_a?(Time)

  result = case value
  when RDF::URI, RDF::Node
    #log_debug("URI | BNode") { value.to_s }
    {'@id' => value.to_s}
  when RDF::Literal
    #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
    res = {}
    if useNativeTypes && [RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double].include?(value.datatype)
      res['@value'] = value.object
      res['@type'] = uri(coerce(property)) 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)).to_s
      elsif value.has_datatype?
        res['@type'] = uri(value.datatype).to_s
      elsif value.has_language? || language(property)
        res['@language'] = (value.language || language(property)).to_s
      end
    end
    res
  else
    # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
    res = {'@value' => value}

    if td.type_mapping
      res['@type'] = td.type_mapping.to_s
    elsif value.is_a?(String)
      if td.language_mapping
        res['@language'] = td.language_mapping
      elsif default_language && td.language_mapping.nil?
        res['@language'] = default_language
      end
    end
    res
  end

  #log_debug("") {"=> #{result.inspect}"}
  result
end

#find_definition(term) ⇒ Term

Find a term definition

Parameters:

  • term (Term, #to_s)

    in unexpanded form

Returns:

  • (Term)


739
740
741
# File 'lib/json/ld/context.rb', line 739

def find_definition(term)
  term.is_a?(TermDefinition) ? term : term_definitions[term.to_s]
end

#from_vocabulary(graph) ⇒ self

Build a context from an RDF::Vocabulary definition.

Examples:

building from an external vocabulary definition


g = RDF::Graph.load("http://schema.org/docs/schema_org_rdfa.html")

context = JSON::LD::Context.new.from_vocabulary(g,
      vocab: "http://schema.org/",
      prefixes: {schema: "http://schema.org/"},
      language: "en")

Parameters:

  • graph (RDF::Queryable)

Returns:

  • (self)


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
711
712
713
# File 'lib/json/ld/context.rb', line 655

def from_vocabulary(graph)
  statements = {}
  ranges = {}

  # Add term definitions for each class and property not in schema:, and
  # for those properties having an object range
  graph.each do |statement|
    next if statement.subject.node?
    (statements[statement.subject] ||= []) << statement

    # Keep track of predicate ranges
    if [RDF::RDFS.range, RDF::SCHEMA.rangeIncludes].include?(statement.predicate) 
      (ranges[statement.subject] ||= []) << statement.object
    end
  end

  # Add term definitions for each class and property not in vocab, and
  # for those properties having an object range
  statements.each do |subject, values|
    types = values.select {|v| v.predicate == RDF.type}.map(&:object)
    is_property = types.any? {|t| t.to_s.include?("Property")}
    
    term = subject.to_s.split(/[\/\#]/).last

    if !is_property
      # Ignore if there's a default voabulary and this is not a property
      next if vocab && subject.to_s.start_with?(vocab)

      # otherwise, create a term definition
      td = term_definitions[term] = TermDefinition.new(term, id: subject.to_s)
    else
      prop_ranges = ranges.fetch(subject, [])
      # If any range is empty or member of range includes rdfs:Literal or schema:Text
      next if vocab && prop_ranges.empty? ||
                       prop_ranges.include?(RDF::SCHEMA.Text) ||
                       prop_ranges.include?(RDF::RDFS.Literal)
      td = term_definitions[term] = TermDefinition.new(term, id: subject.to_s)

      # Set context typing based on first element in range
      case r = prop_ranges.first
      when RDF::XSD.string
        if self.default_language
          td.language_mapping = false
        end
      when RDF::XSD.boolean, RDF::SCHEMA.Boolean, RDF::XSD.date, RDF::SCHEMA.Date,
        RDF::XSD.dateTime, RDF::SCHEMA.DateTime, RDF::XSD.time, RDF::SCHEMA.Time,
        RDF::XSD.duration, RDF::SCHEMA.Duration, RDF::XSD.decimal, RDF::SCHEMA.Number,
        RDF::XSD.float, RDF::SCHEMA.Float, RDF::XSD.integer, RDF::SCHEMA.Integer
        td.type_mapping = r
        td.simple = false
      else
        # It's an object range (includes schema:URL)
        td.type_mapping = '@id'
      end
    end
  end

  self
end

#inspectObject



1207
1208
1209
1210
1211
1212
1213
1214
# File 'lib/json/ld/context.rb', line 1207

def inspect
  v = %w([Context)
  v << "base=#{base}" if base
  v << "vocab=#{vocab}" if vocab
  v << "default_language=#{default_language}" if default_language
  v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
  v.join(" ") + "]"
end

#language(term) ⇒ String

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

Parameters:

  • term (Term, #to_s)

    in unexpanded form

Returns:

  • (String)


759
760
761
762
763
# File 'lib/json/ld/context.rb', line 759

def language(term)
  term = find_definition(term)
  lang = term && term.language_mapping
  lang.nil? ? @default_language : lang
end

#merge(context) ⇒ Context

Merge in a context, creating a new context with updates from ‘context`

Parameters:

Returns:



424
425
426
427
428
# File 'lib/json/ld/context.rb', line 424

def merge(context)
  c = self.dup.merge!(context)
  c.instance_variable_set(:@term_definitions, context.term_definitions.dup)
  c
end

#merge!(context) ⇒ self

Update context with definitions from ‘context`

Parameters:

Returns:

  • (self)


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

def merge!(context)
  # FIXME: if new context removes the default language, this won't do anything
  self.default_language = context.default_language if context.default_language
  self.vocab = context.vocab if context.vocab
  self.base = context.base if context.base

  # Merge in Term Definitions
  term_definitions.merge!(context.term_definitions)
  self
end

#parse(local_context, remote_contexts = []) ⇒ Object

Create an Evaluation Context

When processing a JSON-LD data structure, each processing rule is applied using information provided by the active context. This section describes how to produce an active context.

The active context contains the active term definitions which specify how properties and values have to be interpreted as well as the current base IRI, the vocabulary mapping and the default language. Each term definition consists of an IRI mapping, a boolean flag reverse property, an optional type mapping or language mapping, and an optional container mapping. A term definition can not only be used to map a term to an IRI, but also to map a term to a keyword, in which case it is referred to as a keyword alias.

When processing, the active context is initialized without any term definitions, vocabulary mapping, or default language. If a local context is encountered during processing, a new active context is created by cloning the existing active context. Then the information from the local context is merged into the new active context. Given that local contexts may contain references to remote contexts, this includes their retrieval.

Parameters:

Raises:

  • (JsonLdError)

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

See Also:



302
303
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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/json/ld/context.rb', line 302

def parse(local_context, remote_contexts = [])
  result = self.dup
  result.provided_context = local_context if self.empty?

  local_context = [local_context] unless local_context.is_a?(Array)

  local_context.each do |context|
    case context
    when nil
      # 3.1 If niil, set to a new empty context
      result = Context.new(options)
    when Context
       #log_debug("parse") {"context: #{context.inspect}"}
       result = context.dup
    when IO, StringIO
      #log_debug("parse") {"io: #{context}"}
      # Load context document, if it is an open file
      begin
        ctx = JSON.load(context)
        raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
        result = result.dup.parse(ctx["@context"] ? ctx["@context"].dup : {})
        result.provided_context = ctx["@context"] if [context] == local_context
        result
      rescue JSON::ParserError => e
        #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
        raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
        self.dup
      end
    when String, RDF::URI
      #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}

      # 3.2.1) Set context to the result of resolving value against the base IRI which is established as specified in section 5.1 Establishing a Base URI of [RFC3986]. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
      context = RDF::URI(result.context_base || result.base).join(context)
      context_canon = RDF::URI(context).canonicalize
      context_canon.dup.scheme = 'http'.dup if context_canon.scheme == 'https'

      raise JsonLdError::RecursiveContextInclusion, "#{context}" if remote_contexts.include?(context.to_s)
      remote_contexts << context.to_s

      context_no_base = result.dup
      context_no_base.base = nil
      context_no_base.context_base = context.to_s

      if PRELOADED[context_canon.to_s]
        # If we have a cached context, merge it into the current context (result) and use as the new context
        #log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}

        # If this is a Proc, then replace the entry with the result of running the Proc
        if PRELOADED[context_canon.to_s].respond_to?(:call)
          #log_debug("parse") {"=> (call)"}
          PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
        end
        context = context_no_base.merge!(PRELOADED[context_canon.to_s])
      else

        # Load context document, if it is a string
        begin
          context_opts = @options.dup
          context_opts.delete(:headers)
          @options[:documentLoader].call(context.to_s, context_opts) do |remote_doc|
            # 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
            jo = case remote_doc.document
            when String then MultiJson.load(remote_doc.document)
            else remote_doc.document
            end
            raise JsonLdError::InvalidRemoteContext, "#{context}" unless jo.is_a?(Hash) && jo.has_key?('@context')
            context = jo['@context']
            if @options[:processingMode] == "json-ld-1.0"
              context_no_base.provided_context = context.dup
            end
          end
        rescue JsonLdError::LoadingDocumentFailed => e
          #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
          raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}", e.backtrace
        rescue JsonLdError
          raise
        rescue Exception => e
          #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
          raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}", e.backtrace
        end

        # 3.2.6) Set context to the result of recursively calling this algorithm, passing context no base for active context, context for local context, and remote contexts.
        context = context_no_base.parse(context, remote_contexts.dup)
        context.provided_context = result.provided_context
      end
      context.base ||= result.base
      result = context
      #log_debug("parse") {"=> provided_context: #{context.inspect}"}
    when Hash
      # 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.
      context = context.dup # keep from modifying a hash passed as a param
      {
        '@base' => :base=,
        '@language' => :default_language=,
        '@vocab'    => :vocab=
      }.each do |key, setter|
        v = context.fetch(key, false)
        unless v == false
          context.delete(key)
          #log_debug("parse") {"Set #{key} to #{v.inspect}"}
          result.send(setter, v)
        end
      end

      defined = {}
    # For each key-value pair in context invoke the Create Term Definition subalgorithm, passing result for active context, context for local context, key, and defined
      context.each_key do |key|
        result.create_term_definition(context, key, defined)
      end
    else
      # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
      raise JsonLdError::InvalidLocalContext, context.inspect
    end
  end
  result
end

#reverse?(term) ⇒ Boolean

Is this a reverse term

Parameters:

  • term (Term, #to_s)

    in unexpanded form

Returns:

  • (Boolean)


769
770
771
772
# File 'lib/json/ld/context.rb', line 769

def reverse?(term)
  term = find_definition(term)
  term && term.reverse_property
end

#reverse_term(term) ⇒ Term

Given a term or IRI, find a reverse term definition matching that term. If the term is already reversed, find a non-reversed version.

Parameters:

  • term (Term, #to_s)

Returns:

  • (Term)

    related term definition



779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
# File 'lib/json/ld/context.rb', line 779

def reverse_term(term)
  # Direct lookup of term
  term = term_definitions[term.to_s] if term_definitions.has_key?(term.to_s) && !term.is_a?(TermDefinition)

  # Lookup term, assuming term is an IRI
  unless term.is_a?(TermDefinition)
    td = term_definitions.values.detect {|t| t.id == term.to_s}

    # Otherwise create a temporary term definition
    term = td || TermDefinition.new(term.to_s, id: expand_iri(term, vocab:true))
  end

  # Now, return a term, which reverses this term
  term_definitions.values.detect {|t| t.id == term.id && t.reverse_property != term.reverse_property}
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:

  • (Hash)


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
# File 'lib/json/ld/context.rb', line 607

def serialize(options = {})
  # FIXME: not setting provided_context now
  use_context = case provided_context
  when String, RDF::URI
    #log_debug "serlialize: reuse context: #{provided_context.inspect}"
    provided_context.to_s
  when Hash, Array
    #log_debug "serlialize: reuse context: #{provided_context.inspect}"
    provided_context
  else
    #log_debug("serlialize: generate context")
    #log_debug("") {"=> context: #{inspect}"}
    ctx = {}
    ctx['@base'] = base.to_s if base && base != doc_base
    ctx['@language'] = default_language.to_s if default_language
    ctx['@vocab'] = vocab.to_s if vocab

    # Term Definitions
    term_definitions.keys.sort.each do |term|
      defn = term_definitions[term].to_context_definition(self)
      ctx[term] = defn if defn
    end

    #log_debug("") {"start_doc: context=#{ctx.inspect}"}
    ctx
  end

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

#set_mapping(term, value) ⇒ TermDefinition

Set term mapping

Parameters:

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

Returns:



721
722
723
724
725
726
727
728
729
730
731
732
# File 'lib/json/ld/context.rb', line 721

def set_mapping(term, value)
  #log_debug("") {"map #{term.inspect} to #{value.inspect}"}
  term = term.to_s
  term_definitions[term] = TermDefinition.new(term, id: value)
  term_definitions[term].simple = true

  term_sym = term.empty? ? "" : term.to_sym
  iri_to_term.delete(term_definitions[term].id.to_s) if term_definitions[term].id.is_a?(String)
  @options[:prefixes][term_sym] = value if @options.has_key?(:prefixes)
  iri_to_term[value.to_s] = term
  term_definitions[term]
end

#to_rbString

Turn this into a source for a new instantiation

Returns:

  • (String)


1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
# File 'lib/json/ld/context.rb', line 1188

def to_rb
  defn = []

  defn << "base: #{self.base.to_s.inspect}" if self.base
  defn << "language: #{self.default_language.inspect}" if self.default_language
  defn << "vocab: #{self.vocab.to_s.inspect}" if self.vocab
  term_defs = term_definitions.map do |term, td|
    "      " + term.inspect + " => " + td.to_rb
  end
  defn << "term_definitions: {\n#{term_defs.join(",\n")    }\n    }" unless term_defs.empty?
  %(# -*- encoding: utf-8 -*-
  # frozen_string_literal: true
  # This file generated automatically from #{context_base}
  require 'json/ld'
  class JSON::LD::Context
  ).gsub(/^      /, '') +
  "  add_preloaded(#{RDF::URI(context_base).canonicalize.to_s.inspect}) do\n    new(" + defn.join(", ")  + ")\n  end\nend\n"
end