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 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
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
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
# 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
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 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
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
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
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
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
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
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 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
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
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
# 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 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