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 287
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) 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 263
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.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) click to toggle source

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

# File lib/json/ld/frame.rb, line 209
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 215
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 24
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')
      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]].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, subframe = false, nil
      if !frame.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.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) 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 237
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

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 554
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 567
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 471
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 338
def filter_subject(subject, frame, state, flags)
  # Duck typing, for nodes not having a type, but having @id
  wildcard, matches_some = true, 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 if !flags[:requireAll]
    when '@type'
      # No longer a wildcard pattern
      wildcard = false
      
      match_this = case v
      when []
        # Don't match with any @type
        return false if !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
        elsif (v & node_values).empty?
          # Match on specific @type
          false
        else
          true
        end
      end
      return match_this if !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 = list?(node_values.first) ?
          node_values.first['@list'] :
          false
        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 318
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 485
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 577
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 532
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 506
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 449
def validate_frame(frame)
  raise JsonLdError::InvalidFrame,
        "Invalid JSON-LD frame syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless
    frame.is_a?(Hash) || (frame.is_a?(Array) && frame.first.is_a?(Hash) && frame.length == 1)
  frame = frame.first if frame.is_a?(Array)

  # Check values of @id and @type
  raise JsonLdError::InvalidFrame,
        "Invalid JSON-LD frame syntax; invalid value of @id: #{frame['@id']}" unless
        Array(frame['@id']) == [{}] || Array(frame['@id']).all?{|v| RDF::URI(v).valid?}
  raise JsonLdError::InvalidFrame,
        "Invalid JSON-LD frame syntax; invalid value of @type: #{frame['@type']}" unless
        Array(frame['@type']).all?{|v| v.is_a?(Hash) && (v.keys - %w(@default)).empty? || RDF::URI(v).valid?}
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 589
def value_match?(pattern, value)
  v1, t1, l1 = value['@value'], value['@type'], value['@language']
  v2, t2, l2 = Array(pattern['@value']), Array(pattern['@type']), Array(pattern['@language']).map {|v| v.is_a?(String) ? v.downcase : v}
  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