Module: JSON::LD::Frame

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

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

#cleanup_null(input) ⇒ Array, Hash

Replace ‘@null` with `null`, removing it from arrays.

Parameters:

Returns:



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/json/ld/frame.rb', line 296

def cleanup_null(input)
  case input
  when Array
    # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
    input.map! { |o| cleanup_null(o) }.compact
  when Hash
    input.transform_values do |v|
      cleanup_null(v)
    end
  when '@null'
    # If the value from the key-pair is @null, replace the value with null
    nil
  else
    input
  end
end

#cleanup_preserve(input) ⇒ Array, Hash

Replace @preserve keys with the values, also replace @null with null.

Parameters:

Returns:



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/json/ld/frame.rb', line 273

def cleanup_preserve(input)
  case input
  when Array
    input.map! { |o| cleanup_preserve(o) }
  when Hash
    if input.key?('@preserve')
      # Replace with the content of `@preserve`
      cleanup_preserve(input['@preserve'].first)
    else
      input.transform_values do |v|
        cleanup_preserve(v)
      end
    end
  else
    input
  end
end

#count_blank_node_identifiers(input) ⇒ Hash{String => Integer}

Recursively find and count blankNode identifiers.

Returns:

  • (Hash{String => Integer})


220
221
222
223
224
# File 'lib/json/ld/frame.rb', line 220

def count_blank_node_identifiers(input)
  {}.tap do |results|
    count_blank_node_identifiers_internal(input, results)
  end
end

#count_blank_node_identifiers_internal(input, results) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/json/ld/frame.rb', line 226

def count_blank_node_identifiers_internal(input, results)
  case input
  when Array
    input.each { |o| count_blank_node_identifiers_internal(o, results) }
  when Hash
    input.each do |_k, v|
      count_blank_node_identifiers_internal(v, results)
    end
  when String
    if input.start_with?('_:')
      results[input] ||= 0
      results[input] += 1
    end
  end
end

#frame(state, subjects, frame, parent: nil, property: nil, ordered: false, **options) ⇒ Object

Frame input. Input is expected in expanded form, but frame is in compacted form.

Parameters:

  • state (Hash{Symbol => Object})

    Current framing state

  • subjects (Array<String>)

    The subjects to filter

  • frame (Hash{String => Object})
  • property (String) (defaults to: nil)

    (nil) The parent property.

  • parent (Hash{String => Object}) (defaults to: nil)

    (nil) Parent subject or top-level array

  • ordered (Boolean) (defaults to: false)

    (true) Ensure output objects have keys ordered properly

  • options (Hash{Symbol => Object})

    ({})

Raises:

  • (JSON::LD::InvalidFrame)


26
27
28
29
30
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
# File 'lib/json/ld/frame.rb', line 26

def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, **options)
  # Validate the frame
  validate_frame(frame)
  frame = frame.first if frame.is_a?(Array)

  # Get values for embedOn and explicitOn
  flags = {
    embed: get_frame_flag(frame, options, :embed),
    explicit: get_frame_flag(frame, options, :explicit),
    requireAll: get_frame_flag(frame, options, :requireAll)
  }

  # Get link for current graph
  link = state[:link][state[:graph]] ||= {}

  # Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
  # This gives us a hash of objects indexed by @id
  matches = filter_subjects(state, subjects, frame, flags)

  # For each id and node from the set of matched subjects ordered by id
  matches.keys.opt_sort(ordered: ordered).each do |id|
    subject = matches[id]

    # NOTE: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is nil, which only occurs at the top-level.
    if property.nil?
      state[:uniqueEmbeds] = { state[:graph] => {} }
    else
      state[:uniqueEmbeds][state[:graph]] ||= {}
    end

    if flags[:embed] == '@link' && link.key?(id)
      # add existing linked subject
      add_frame_output(parent, property, link[id])
      next
    end

    output = { '@id' => id }
    link[id] = output

    if %w[@first @last].include?(flags[:embed]) && context.processingMode('json-ld-1.1')
      if @options[:validate]
        raise JSON::LD::JsonLdError::InvalidEmbedValue,
          "#{flags[:embed]} is not a valid value of @embed in 1.1 mode"
      end

      warn "[DEPRECATION] #{flags[:embed]}  is not a valid value of @embed in 1.1 mode.\n"
    end

    if !state[:embedded] && state[:uniqueEmbeds][state[:graph]].key?(id)
      # Skip adding this node object to the top-level, as it was included in another node object
      next
    elsif state[:embedded] &&
          (flags[:embed] == '@never' || creates_circular_reference(subject, state[:graph], state[:subjectStack]))
      # if embed is @never or if a circular reference would be created by an embed, the subject cannot be embedded, just add the reference; note that a circular reference won't occur when the embed flag is `@link` as the above check will short-circuit before reaching this point
      add_frame_output(parent, property, output)
      next
    elsif state[:embedded] &&
          %w[@first @once].include?(flags[:embed]) &&
          state[:uniqueEmbeds][state[:graph]].key?(id)

      # if only the first match should be embedded
      # Embed unless already embedded
      add_frame_output(parent, property, output)
      next
    elsif flags[:embed] == '@last'
      # if only the last match should be embedded
      # remove any existing embed
      remove_embed(state, id) if state[:uniqueEmbeds][state[:graph]].include?(id)
    end

    state[:uniqueEmbeds][state[:graph]][id] = {
      parent: parent,
      property: property
    }

    # push matching subject onto stack to enable circular embed checks
    state[:subjectStack] << { subject: subject, graph: state[:graph] }

    # Subject is also the name of a graph
    if state[:graphMap].key?(id)
      # check frame's "@graph" to see what to do next
      # 1. if it doesn't exist and state.graph === "@merged", don't recurse
      # 2. if it doesn't exist and state.graph !== "@merged", recurse
      # 3. if "@merged" then don't recurse
      # 4. if "@default" then don't recurse
      # 5. recurse
      recurse = false
      subframe = nil
      if frame.key?('@graph')
        subframe = frame['@graph'].first
        recurse = !['@merged', '@default'].include?(id)
        subframe = {} unless subframe.is_a?(Hash)
      else
        recurse = (state[:graph] != '@merged')
        subframe = {}
      end

      if recurse
        frame(state.merge(graph: id, embedded: false), state[:graphMap][id].keys, [subframe], parent: output,
          property: '@graph', **options)
      end
    end

    # If frame has `@included`, recurse over its sub-frame
    if frame['@included']
      frame(state.merge(embedded: false), subjects, frame['@included'], parent: output, property: '@included',
  **options)
    end

    # iterate over subject properties in order
    subject.keys.opt_sort(ordered: ordered).each do |prop|
      objects = subject[prop]

      # copy keywords to output
      if prop.start_with?('@')
        output[prop] = objects.dup
        next
      end

      # explicit is on and property isn't in frame, skip processing
      next if flags[:explicit] && !frame.key?(prop)

      # add objects
      objects.each do |o|
        subframe = Array(frame[prop]).first || create_implicit_frame(flags)

        if list?(o)
          subframe = frame[prop].first['@list'] if Array(frame[prop]).first.is_a?(Hash)
          subframe ||= create_implicit_frame(flags)
          # add empty list
          list = { '@list' => [] }
          add_frame_output(output, prop, list)

          src = o['@list']
          src.each do |oo|
            if node_reference?(oo)
              frame(state.merge(embedded: true), [oo['@id']], subframe, parent: list, property: '@list',
**options)
            else
              add_frame_output(list, '@list', oo.dup)
            end
          end
        elsif node_reference?(o)
          # recurse into subject reference
          frame(state.merge(embedded: true), [o['@id']], subframe, parent: output, property: prop, **options)
        elsif value_match?(subframe, o)
          # Include values if they match
          add_frame_output(output, prop, o.dup)
        end
      end
    end

    # handle defaults in order
    frame.keys.opt_sort(ordered: ordered).each do |prop|
      if prop == '@type' && frame[prop].first.is_a?(Hash) && frame[prop].first.keys == %w[@default]
        # Treat this as a default
      elsif prop.start_with?('@')
        next
      end

      # if omit default is off, then include default values for properties that appear in the next frame but are not in the matching subject
      n = frame[prop].first || {}
      omit_default_on = get_frame_flag(n, options, :omitDefault)
      if !omit_default_on && !output[prop]
        preserve = as_array(n.fetch('@default', '@null').dup)
        output[prop] = [{ '@preserve' => preserve }]
      end
    end

    # If frame has @reverse, embed identified nodes having this subject as a value of the associated property.
    frame.fetch('@reverse', {}).each do |reverse_prop, subframe|
      state[:subjects].each do |r_id, node|
        next unless Array(node[reverse_prop]).any? { |v| v['@id'] == id }

        # Node has property referencing this subject
        # recurse into  reference
        (output['@reverse'] ||= {})[reverse_prop] ||= []
        frame(state.merge(embedded: true), [r_id], subframe, parent: output['@reverse'][reverse_prop],
          property: property, **options)
      end
    end

    # add output to parent
    add_frame_output(parent, property, output)

    # pop matching subject from circular ref-checking stack
    state[:subjectStack].pop
  end
  # end
end

#prune_bnodes(input, bnodes_to_clear) ⇒ Array, Hash

Prune BNode identifiers recursively

Parameters:

  • input (Array, Hash)
  • bnodes_to_clear (Array<String>)

Returns:



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/json/ld/frame.rb', line 248

def prune_bnodes(input, bnodes_to_clear)
  case input
  when Array
    # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
    input.map { |o| prune_bnodes(o, bnodes_to_clear) }.compact
  when Hash
    output = {}
    input.each do |key, value|
      if context.expand_iri(key) == '@id' && bnodes_to_clear.include?(value)
        # Don't add this to output, as it is pruned as being superfluous
      else
        output[key] = prune_bnodes(value, bnodes_to_clear)
      end
    end
    output
  else
    input
  end
end

#remove_dependents(id, embeds) ⇒ Object

recursively remove dependent dangling embeds



547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/json/ld/frame.rb', line 547

def remove_dependents(id, embeds)
  # get embed keys as a separate array to enable deleting keys in map
  embeds.each do |id_dep, e|
    p = e.fetch(:parent, {}) if e.is_a?(Hash)
    next unless p.is_a?(Hash)

    pid = p.fetch('@id', nil)
    if pid == id
      embeds.delete(id_dep)
      remove_dependents(id_dep, embeds)
    end
  end
end