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_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_preserve(input, bnodes_to_clear) ⇒ Array, Hash

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

Optionally, remove BNode identifiers only used once.

Parameters:

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

Returns:



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

def cleanup_preserve(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.
    v = input.map {|o| cleanup_preserve(o, bnodes_to_clear)}.compact

    # If the array contains a single member, which is itself an array, use that value as the result
    (v.length == 1 && v.first.is_a?(Array)) ? v.first : v
  when Hash
    output = Hash.new
    input.each do |key, value|
      if key == '@preserve'
        # replace all key-value pairs where the key is @preserve with the value from the key-pair
        output = cleanup_preserve(value, bnodes_to_clear)
      elsif context.expand_iri(key) == '@id' && bnodes_to_clear.include?(value)
        # Don't add this to output, as it is pruned as being superfluous
      else
        v = cleanup_preserve(value, bnodes_to_clear)

        # Because we may have added a null value to an array, we need to clean that up, if we possible
        v = v.first if v.is_a?(Array) && v.length == 1 && !context.as_array?(key)
        output[key] = v
      end
    end
    output
  when '@null'
    # If the value from the key-pair is @null, replace the value with nul
    nil
  else
    input
  end
  result
end

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

Recursively find and count blankNode identifiers.

Returns:

  • (Hash{String => Integer})


194
195
196
197
198
# File 'lib/json/ld/frame.rb', line 194

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



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/json/ld/frame.rb', line 200

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, **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})
  • options (Hash{Symbol => Object})

    ({})

Options Hash (**options):

  • :parent (Hash{String => Object}) — default: nil

    Parent subject or top-level array

  • :property (String) — default: nil

    The parent property.

Raises:

  • (JSON::LD::InvalidFrame)


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

def frame(state, subjects, frame, **options)
  log_depth do
  log_debug("frame") {"subjects: #{subjects.inspect}"}
  log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
  log_debug("frame") {"property: #{options[:property].inspect}"}

  parent, property = options[:parent], options[:property]
  # 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.kw_sort.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 None, 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 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
    if flags[:embed] == '@never' || creates_circular_reference(subject, state[:graph], state[:subjectStack])
      add_frame_output(parent, property, output)
      next
    end

    # if only the last match should be embedded
    if flags[:embed] == '@last'
      # remove any existing embed
      remove_embed(state, id) if state[:uniqueEmbeds][state[:graph]].include?(id)
      state[:uniqueEmbeds][state[:graph]][id] = {
        parent: parent,
        property: property
      }
    end

    # 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)
      log_debug("frame") {"#{id} in graphMap"}
      # 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
        state[:graphStack].push(state[:graph])
        state[:graph] = id
        frame(state, state[:graphMap][id].keys, [subframe], options.merge(parent: output, property: '@graph'))
        state[:graph] = state[:graphStack].pop
      end
    end

    # iterate over subject properties in order
    subject.keys.kw_sort.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, [oo['@id']], subframe, options.merge(parent: list, property: '@list'))
            else
              add_frame_output(list, '@list', oo.dup)
            end
          end
        when node_reference?(o)
          # recurse into subject reference
          frame(state, [o['@id']], subframe, options.merge(parent: output, property: prop))
        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.kw_sort.each do |prop|
      next if prop.start_with?('@')

      # 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 = n.fetch('@default', '@null').dup
        preserve = [preserve] unless preserve.is_a?(Array)
        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, [r_id], subframe, options.merge(parent: output['@reverse'][reverse_prop]))
        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

#remove_dependents(id, embeds) ⇒ Object

recursively remove dependent dangling embeds



467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/json/ld/frame.rb', line 467

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