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_property, #has_value, #index?, #list?, #node?, #node_or_ref?, #node_reference?, #simple_graph?, #value?

Instance Method Details

#cleanup_null(input) ⇒ Array, Hash

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

Parameters:

Returns:



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/json/ld/frame.rb', line 288

def cleanup_null(input)
  result = 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.inject({}) do |memo, (k,v)|
      memo.merge(k => cleanup_null(v))
    end
  when '@null'
    # If the value from the key-pair is @null, replace the value with null
    nil
  else
    input
  end
  result
end

#cleanup_preserve(input) ⇒ Array, Hash

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

Parameters:

Returns:



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/json/ld/frame.rb', line 264

def cleanup_preserve(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_preserve(o)}
  when Hash
    if input.has_key?('@preserve')
      # Replace with the content of `@preserve`
      cleanup_preserve(input['@preserve'].first)
    else
      input.inject({}) do |memo, (k,v)|
        memo.merge(k => 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})


210
211
212
213
214
# File 'lib/json/ld/frame.rb', line 210

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



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/json/ld/frame.rb', line 216

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)


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

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.has_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')
      raise JSON::LD::JsonLdError::InvalidEmbedValue, "#{flags[:embed]} is not a valid value of @embed in 1.1 mode" if @options[:validate]
      warn "[DEPRECATION] #{flags[:embed]}  is not a valid value of @embed in 1.1 mode.\n"
    end

    if !state[:embedded] && state[:uniqueEmbeds][state[:graph]].has_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]].has_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].has_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, subframe = false, nil
      if !frame.has_key?('@graph')
        recurse, subframe = (state[:graph] != '@merged'), {}
      else
        subframe = frame['@graph'].first
        recurse = !(id == '@merged' || id == '@default')
        subframe = {} unless subframe.is_a?(Hash)
      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.has_key?(prop)

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

        case
        when 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
        when node_reference?(o)
          # recurse into subject reference
          frame(state.merge(embedded: true), [o['@id']], subframe, parent: output, property: prop, **options)
        when 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|
        if 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
    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:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/json/ld/frame.rb', line 238

def prune_bnodes(input, bnodes_to_clear)
  result = 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 = Hash.new
    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
  result
end

#remove_dependents(id, embeds) ⇒ Object

recursively remove dependent dangling embeds



533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/json/ld/frame.rb', line 533

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