module JSON::LD::Frame

Constants

FRAMING_KEYWORDS

Public Instance Methods

cleanup_null(input) click to toggle source

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

@param [Array, Hash] input @return [Array, Hash]

# 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) click to toggle source

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

@param [Array, Hash] input @return [Array, Hash]

# 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) click to toggle source

Recursively find and count blankNode identifiers. @return [Hash{String => Integer}]

# 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) click to toggle source
# 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) click to toggle source

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

@param [Hash{Symbol => Object}] state

Current framing state

@param [Array<String>] subjects

The subjects to filter

@param [Hash{String => Object}] frame @param [String] property (nil)

The parent property.

@param [Hash{String => Object}] parent (nil)

Parent subject or top-level array

@param [Boolean] ordered (true)

Ensure output objects have keys ordered properly

@param [Hash{Symbol => Object}] options ({}) @raise [JSON::LD::InvalidFrame]

# 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) click to toggle source

Prune BNode identifiers recursively

@param [Array, Hash] input @param [Array<String>] bnodes_to_clear @return [Array, Hash]

# 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

Private Instance Methods

add_frame_output(parent, property, output) click to toggle source

Adds framing output to the given parent.

@param parent the parent to add to. @param property the parent property, null for an array parent. @param output the output to add.

# File lib/json/ld/frame.rb, line 570
def add_frame_output(parent, property, output)
  if parent.is_a?(Hash)
    parent[property] ||= []
    parent[property] << output
  else
    parent << output
  end
end
create_implicit_frame(flags) click to toggle source

Creates an implicit frame when recursing through subject matches. If a frame doesn’t have an explicit frame for a particular property, then a wildcard child frame will be created that uses the same flags that the parent frame used.

@param [Hash] flags the current framing flags. @return [Array<Hash>] the implicit frame.

# File lib/json/ld/frame.rb, line 583
def create_implicit_frame(flags)
  {}.tap do |memo|
    flags.each_pair do |key, val|
      memo["@#{key}"] = [val]
    end
  end
end
creates_circular_reference(subject_to_embed, graph, subject_stack) click to toggle source

Checks the current subject stack to see if embedding the given subject would cause a circular reference.

@param subject_to_embed the subject to embed. @param graph the graph the subject to embed is in. @param subject_stack the current stack of subjects.

@return true if a circular reference would be created, false if not.

# File lib/json/ld/frame.rb, line 486
def creates_circular_reference(subject_to_embed, graph, subject_stack)
  subject_stack[0..-2].any? do |subject|
    subject[:graph] == graph && subject[:subject]['@id'] == subject_to_embed['@id']
  end
end
filter_subject(subject, frame, state, flags) click to toggle source

Returns true if the given node matches the given frame.

Matches either based on explicit type inclusion where the node has any type listed in the frame. If the frame has empty types defined matches nodes not having a @type. If the frame has a type of {} defined matches nodes having any type defined.

Otherwise, does duck typing, where the node must have any or all of the properties defined in the frame, depending on the ‘requireAll` flag.

@param [Hash{String => Object}] subject the subject to check. @param [Hash{String => Object}] frame the frame to check. @param [Hash{Symbol => Object}] state Current framing state @param [Hash{Symbol => Object}] flags the frame flags.

@return [Boolean] true if the node matches, false if not.

# File lib/json/ld/frame.rb, line 346
def filter_subject(subject, frame, state, flags)
  # Duck typing, for nodes not having a type, but having @id
  wildcard = true
  matches_some = false

  frame.each do |k, v|
    node_values = subject.fetch(k, [])

    case k
    when '@id'
      ids = v || []

      # Match on specific @id.
      match_this = case ids
      when [], [{}]
        # Match on no @id or any @id
        true
      else
        # Match on specific @id
        ids.include?(subject['@id'])
      end
      return match_this unless flags[:requireAll]
    when '@type'
      # No longer a wildcard pattern
      wildcard = false

      match_this = case v
      when []
        # Don't match with any @type
        return false unless node_values.empty?

        true
      when [{}]
        # Match with any @type
        !node_values.empty?
      else
        # Treat a map with @default like an empty map
        if v.first.is_a?(Hash) && v.first.keys == %w[@default]
          true
        else
          !(v & node_values).empty?
        end
      end
      return match_this unless flags[:requireAll]
    when /@/
      # Skip other keywords
      next
    else
      is_empty = v.empty?
      if (v = v.first)
        validate_frame(v)
        has_default = v.key?('@default')
      end

      # No longer a wildcard pattern if frame has any non-keyword properties
      wildcard = false

      # Skip, but allow match if node has no value for property, and frame has a default value
      next if node_values.empty? && has_default

      # If frame value is empty, don't match if subject has any value
      return false if !node_values.empty? && is_empty

      match_this = case
      when v.nil?
        # node does not match if values is not empty and the value of property in frame is match none.
        return false unless node_values.empty?

        true
      when v.is_a?(Hash) && (v.keys - FRAMING_KEYWORDS).empty?
        # node matches if values is not empty and the value of property in frame is wildcard (frame with properties other than framing keywords)
        !node_values.empty?
      when value?(v)
        # Match on any matching value
        node_values.any? { |nv| value_match?(v, nv) }
      when node?(v) || node_reference?(v)
        node_values.any? do |nv|
          node_match?(v, nv, state, flags)
        end
      when list?(v)
        vv = v['@list'].first
        node_values = if list?(node_values.first)
          node_values.first['@list']
        else
          false
        end
        if !node_values
          false # Lists match Lists
        elsif value?(vv)
          # Match on any matching value
          node_values.any? { |nv| value_match?(vv, nv) }
        elsif node?(vv) || node_reference?(vv)
          node_values.any? do |nv|
            node_match?(vv, nv, state, flags)
          end
        else
          false
        end
      else
        false # No matching on non-value or node values
      end
    end

    # All non-defaulted values must match if @requireAll is set
    return false if !match_this && flags[:requireAll]

    matches_some ||= match_this
  end

  # return true if wildcard or subject matches some properties
  wildcard || matches_some
end
filter_subjects(state, subjects, frame, flags) click to toggle source

Returns a map of all of the subjects that match a parsed frame.

@param [Hash{Symbol => Object}] state

Current framing state

@param [Array<String>] subjects

The subjects to filter

@param [Hash{String => Object}] frame @param [Hash{Symbol => String}] flags the frame flags.

@return all of the matched subjects.

# File lib/json/ld/frame.rb, line 326
def filter_subjects(state, subjects, frame, flags)
  subjects.each_with_object({}) do |id, memo|
    subject = state[:graphMap][state[:graph]][id]
    memo[id] = subject if filter_subject(subject, frame, state, flags)
  end
end
get_frame_flag(frame, options, name) click to toggle source

Gets the frame flag value for the given flag name.

@param frame the frame. @param options the framing options. @param name the flag name.

@return the flag value.

# File lib/json/ld/frame.rb, line 500
def get_frame_flag(frame, options, name)
  rval = frame.fetch("@#{name}", [options[name]]).first
  rval = rval.values.first if value?(rval)
  if name == :embed
    rval = case rval
    when true then '@once'
    when false then '@never'
    when '@always', '@first', '@last', '@link', '@once', '@never' then rval
    else
      raise JsonLdError::InvalidEmbedValue,
        "Invalid JSON-LD frame syntax; invalid value of @embed: #{rval}"
    end
  end
  rval
end
node_match?(pattern, value, state, flags) click to toggle source

Node matches if it is a node, and matches the pattern as a frame

# File lib/json/ld/frame.rb, line 592
def node_match?(pattern, value, state, flags)
  return false unless value['@id']

  node_object = state[:subjects][value['@id']]
  node_object && filter_subject(node_object, pattern, state, flags)
end
remove_dependents(id, embeds) click to toggle source

recursively remove dependent dangling embeds

# 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
remove_embed(state, id) click to toggle source

Removes an existing embed.

@param state the current framing state. @param id the @id of the embed to remove.

# File lib/json/ld/frame.rb, line 521
def remove_embed(state, id)
  # get existing embed
  embeds = state[:uniqueEmbeds][state[:graph]]
  embed = embeds[id]
  property = embed[:property]

  # create reference to replace embed
  subject = { '@id' => id }

  if embed[:parent].is_a?(Array)
    # replace subject with reference
    embed[:parent].map! do |parent|
      compare_values(parent, subject) ? subject : parent
    end
  else
    parent = embed[:parent]
    # replace node with reference
    if parent[property].is_a?(Array)
      parent[property].reject! { |v| compare_values(v, subject) }
      parent[property] << subject
    elsif compare_values(parent[property], subject)
      parent[property] = subject
    end
  end

  # recursively remove dependent dangling embeds
  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

  remove_dependents(id, embeds)
end
validate_frame(frame) click to toggle source
# File lib/json/ld/frame.rb, line 459
def validate_frame(frame)
  unless frame.is_a?(Hash) || (frame.is_a?(Array) && frame.first.is_a?(Hash) && frame.length == 1)
    raise JsonLdError::InvalidFrame,
      "Invalid JSON-LD frame syntax; a JSON-LD frame must be an object: #{frame.inspect}"
  end
  frame = frame.first if frame.is_a?(Array)

  # Check values of @id and @type
  unless Array(frame['@id']) == [{}] || Array(frame['@id']).all? { |v| RDF::URI(v).valid? }
    raise JsonLdError::InvalidFrame,
      "Invalid JSON-LD frame syntax; invalid value of @id: #{frame['@id']}"
  end
  unless Array(frame['@type']).all? do |v|
           (v.is_a?(Hash) && (v.keys - %w[@default]).empty?) || RDF::URI(v).valid?
         end
    raise JsonLdError::InvalidFrame,
      "Invalid JSON-LD frame syntax; invalid value of @type: #{frame['@type']}"
  end
end
value_match?(pattern, value) click to toggle source

Value matches if it is a value, and matches the value pattern.

  • ‘pattern` is empty

  • @values are the same, or ‘pattern` is a wildcard, and

  • @types are the same or ‘value` is not null and `pattern` is `{}`, or `value` is null and `pattern` is null or `[]`, and

  • @languages are the same or ‘value` is not null and `pattern` is `{}`, or `value` is null and `pattern` is null or `[]`.

# File lib/json/ld/frame.rb, line 605
def value_match?(pattern, value)
  v1 = value['@value']
  t1 = value['@type']
  l1 = value['@language']
  v2 = Array(pattern['@value'])
  t2 = Array(pattern['@type'])
  l2 = Array(pattern['@language']).map do |v|
    v.is_a?(String) ? v.downcase : v
  end
  return true if (v2 + t2 + l2).empty?
  return false unless v2.include?(v1) || v2 == [{}]
  return false unless t2.include?(t1) || (t1 && t2 == [{}]) || (t1.nil? && (t2 || []).empty?)
  return false unless l2.include?(l1.to_s.downcase) || (l1 && l2 == [{}]) || (l1.nil? && (l2 || []).empty?)

  true
end