module JSON::LD::Frame
Constants
- FRAMING_KEYWORDS
Public Instance Methods
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
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
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
# 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
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 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
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
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
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
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
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
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 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
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
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
# 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 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