Module: JSON::LD::Expand

Includes:
Utils
Included in:
API
Defined in:
lib/json/ld/expand.rb

Overview

Expand module, used as part of API

Constant Summary collapse

CONTAINER_INDEX_ID_TYPE =

The following constant is used to reduce object allocations

Set['@index', '@id', '@type'].freeze
KEY_ID =
%w[@id].freeze
KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION =
%w[@value @language @type @index @direction @annotation].freeze
KEYS_SET_LIST_INDEX =
%w[@set @list @index].freeze
KEYS_INCLUDED_TYPE_REVERSE =
%w[@included @type @reverse].freeze

Instance Method Summary collapse

Methods included from Utils

#add_value, #as_array, #as_resource, #blank_node?, #compare_values, #graph?, #has_value?, #index?, #list?, #node?, #node_or_ref?, #node_reference?, #property?, #simple_graph?, #value?

Instance Method Details

#expand(input, active_property, context, framing: false, from_map: false, log_depth: nil) ⇒ Array<Hash{String => Object}>

Expand an Array or Object given an active context and performing local context expansion.

Parameters:

  • input (Array, Hash)
  • active_property (String)
  • context (Context)
  • framing (Boolean) (defaults to: false)

    (false) Special rules for expanding a frame

  • from_map (Boolean) (defaults to: false)

    Expanding from a map, which could be an ‘@type` map, so don’t clear out context term definitions

Returns:

  • (Array<Hash{String => Object}>)


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
# File 'lib/json/ld/expand.rb', line 31

def expand(input, active_property, context,
           framing: false, from_map: false, log_depth: nil)
  # log_debug("expand", depth: log_depth.to_i) {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
  framing = false if active_property == '@default'
  if active_property
    expanded_active_property = context.expand_iri(active_property, vocab: true, as_string: true,
      base: @options[:base])
  end

  # Use a term-specific context, if defined, based on the non-type-scoped context.
  if active_property && context.term_definitions[active_property]
    property_scoped_context = context.term_definitions[active_property].context
  end
  # log_debug("expand", depth: log_depth.to_i) {"property_scoped_context: #{property_scoped_context.inspect}"} unless property_scoped_context.nil?

  case input
  when Array
    # If element is an array,
    is_list = context.container(active_property).include?('@list')
    input.each_with_object([]) do |v, memo|
      # Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
      v = expand(v, active_property, context,
        framing: framing,
        from_map: from_map,
        log_depth: log_depth.to_i + 1)

      # If the active property is @list or its container mapping is set to @list and v is an array, change it to a list object
      if is_list && v.is_a?(Array)
        # Make sure that no member of v contains an annotation object
        if v.any? { |n| n.is_a?(Hash) && n.key?('@annotation') }
          raise JsonLdError::InvalidAnnotation,
            "A list element must not contain @annotation."
        end
        v = { "@list" => v }
      end

      case v
      when nil then nil
      when Array then memo.concat(v)
      else            memo << v
      end
    end

  when Hash
    if context.previous_context
      expanded_key_map = input.keys.inject({}) do |memo, key|
        memo.merge(key => context.expand_iri(key, vocab: true, as_string: true, base: @options[:base]))
      end
      # Revert any previously type-scoped term definitions, unless this is from a map, a value object or a subject reference
      revert_context = !from_map &&
                       !expanded_key_map.value?('@value') &&
                       expanded_key_map.values != ['@id']

      # If there's a previous context, the context was type-scoped
      # log_debug("expand", depth: log_depth.to_i) {"previous_context: #{context.previous_context.inspect}"} if revert_context
      context = context.previous_context if revert_context
    end

    # Apply property-scoped context after reverting term-scoped context
    unless property_scoped_context.nil?
      context = context.parse(property_scoped_context, base: @options[:base], override_protected: true)
    end
    # log_debug("expand", depth: log_depth.to_i) {"after property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?

    # If element contains the key @context, set active context to the result of the Context Processing algorithm, passing active context and the value of the @context key as local context.
    if input.key?('@context')
      context = context.parse(input['@context'], base: @options[:base])
      # log_debug("expand", depth: log_depth.to_i) {"context: #{context.inspect}"}
    end

    # Set the type-scoped context to the context on input, for use later
    type_scoped_context = context

    output_object = {}

    # See if keys mapping to @type have terms with a local context
    type_key = nil
    (input.keys - %w[@context]).sort
      .select { |k| context.expand_iri(k, vocab: true, base: @options[:base]) == '@type' }
      .each do |tk|
      type_key ||= tk # Side effect saves the first found key mapping to @type
      Array(input[tk]).sort.each do |term|
        if type_scoped_context.term_definitions[term]
          term_context = type_scoped_context.term_definitions[term].context
        end
        unless term_context.nil?
          # log_debug("expand", depth: log_depth.to_i) {"term_context[#{term}]: #{term_context.inspect}"}
          context = context.parse(term_context, base: @options[:base], propagate: false)
        end
      end
    end

    # Process each key and value in element. Ignores @nesting content
    expand_object(input, active_property, context, output_object,
      expanded_active_property: expanded_active_property,
      framing: framing,
      type_key: type_key,
      type_scoped_context: type_scoped_context,
      log_depth: log_depth.to_i + 1)

    # log_debug("output object", depth: log_depth.to_i) {output_object.inspect}

    # If result contains the key @value:
    if value?(output_object)
      keys = output_object.keys
      unless (keys - KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION).empty?
        # The result must not contain any keys other than @direction, @value, @language, @type, and @index. It must not contain both the @language key and the @type key. Otherwise, an invalid value object error has been detected and processing is aborted.
        raise JsonLdError::InvalidValueObject,
          "value object has unknown keys: #{output_object.inspect}"
      end

      if keys.include?('@type') && !(keys & %w[@language @direction]).empty?
        # @type is inconsistent with either @language or @direction
        raise JsonLdError::InvalidValueObject,
          "value object must not include @type with either @language or @direction: #{output_object.inspect}"
      end

      if output_object.key?('@language') && Array(output_object['@language']).empty?
        output_object.delete('@language')
      end
      type_is_json = output_object['@type'] == '@json'
      output_object.delete('@type') if output_object.key?('@type') && Array(output_object['@type']).empty?

      # If the value of result's @value key is null, then set result to null and @type is not @json.
      ary = Array(output_object['@value'])
      return nil if ary.empty? && !type_is_json

      if output_object['@type'] == '@json' && context.processingMode('json-ld-1.1')
        # Any value of @value is okay if @type: @json
      elsif !ary.all? { |v| v.is_a?(String) || (v.is_a?(Hash) && v.empty?) } && output_object.key?('@language')
        # Otherwise, if the value of result's @value member is not a string and result contains the key @language, an invalid language-tagged value error has been detected (only strings can be language-tagged) and processing is aborted.
        raise JsonLdError::InvalidLanguageTaggedValue,
          "when @language is used, @value must be a string: #{output_object.inspect}"
      elsif output_object['@type'] &&
            (!Array(output_object['@type']).all? do |t|
               (t.is_a?(String) && RDF::URI(t).valid? && !t.start_with?('_:')) ||
               (t.is_a?(Hash) && t.empty?)
             end ||
             (!framing && !output_object['@type'].is_a?(String)))
        # Otherwise, if the result has a @type member and its value is not an IRI, an invalid typed value error has been detected and processing is aborted.
        raise JsonLdError::InvalidTypedValue,
          "value of @type must be an IRI or '@json': #{output_object.inspect}"
      elsif !framing && !output_object.fetch('@type', '').is_a?(String) &&
            RDF::URI(t).valid? && !t.start_with?('_:')
        # Otherwise, if the result has a @type member and its value is not an IRI, an invalid typed value error has been detected and processing is aborted.
        raise JsonLdError::InvalidTypedValue,
          "value of @type must be an IRI or '@json': #{output_object.inspect}"
      end
    elsif !output_object.fetch('@type', []).is_a?(Array)
      # Otherwise, if result contains the key @type and its associated value is not an array, set it to an array containing only the associated value.
      output_object['@type'] = [output_object['@type']]
    elsif output_object.key?('@set') || output_object.key?('@list')
      # Otherwise, if result contains the key @set or @list:
      # The result must contain at most one other key and that key must be @index. Otherwise, an invalid set or list object error has been detected and processing is aborted.
      unless (output_object.keys - KEYS_SET_LIST_INDEX).empty?
        raise JsonLdError::InvalidSetOrListObject,
          "@set or @list may only contain @index: #{output_object.keys.inspect}"
      end

      # If result contains the key @set, then set result to the key's associated value.
      return output_object['@set'] if output_object.key?('@set')
    elsif output_object['@annotation']
      # Otherwise, if result contains the key @annotation,
      # the array value must all be node objects without an @id property, otherwise, an invalid annotation error has been detected and processing is aborted.
      unless output_object['@annotation'].all? { |o| node?(o) && !o.key?('@id') }
        raise JsonLdError::InvalidAnnotation,
          "@annotation must reference node objects without @id."
      end

      # Additionally, the property must not be used if there is no active property, or the expanded active property is @graph.
      if %w[@graph @included].include?(expanded_active_property || '@graph')
        raise JsonLdError::InvalidAnnotation,
          "@annotation must not be used on a top-level object."
      end

    end

    # If result contains only the key @language, set result to null.
    return nil if output_object.length == 1 && output_object.key?('@language')

    # If active property is null or @graph, drop free-floating values as follows:
    if (expanded_active_property || '@graph') == '@graph' &&
       (output_object.key?('@value') || output_object.key?('@list') ||
       ((output_object.keys - KEY_ID).empty? && !framing))
      # log_debug(" =>", depth: log_depth.to_i) { "empty top-level: " + output_object.inspect}
      return nil
    end

    # Re-order result keys if ordering
    if @options[:ordered]
      output_object.keys.sort.each_with_object({}) { |kk, memo| memo[kk] = output_object[kk] }
    else
      output_object
    end
  else
    # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
    return nil if input.nil? || active_property.nil? || expanded_active_property == '@graph'

    # Apply property-scoped context
    unless property_scoped_context.nil?
      context = context.parse(property_scoped_context,
        base: @options[:base],
        override_protected: true)
    end
    # log_debug("expand", depth: log_depth.to_i) {"property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil?

    context.expand_value(active_property, input, base: @options[:base])
  end

  # log_debug(depth: log_depth.to_i) {" => #{result.inspect}"}
end