lib/facter/framework/parsers/query_parser.rb
# frozen_string_literal: true
module Facter
class QueryParser
@log = Log.new(self)
class << self
# Searches for facts that could resolve a user query.
# There are 4 types of facts:
# root facts
# e.g. networking
# child facts
# e.g. networking.dhcp
# composite facts
# e.g. networking.interfaces.en0.bindings.address
# regex facts (legacy)
# e.g. impaddress_end160
#
# Because a root fact will always be resolved by a collection of child facts,
# we can return one or more child facts for each parent.
#
# @param query_list [Array] The list of facts to search for
# @param loaded_facts [Array] All of the fact definitions for the current operating system
#
# @return [Array<SearchedFact>] a list of searchable facts that resolve the user's query
def parse(query_list, loaded_facts)
matched_facts = []
@query_list = query_list
return no_user_query(loaded_facts) unless query_list.any?
query_list.each do |query|
found_facts = search_for_facts(query, loaded_facts)
matched_facts << found_facts
end
matched_facts.flatten(1)
end
def no_user_query(loaded_facts)
searched_facts = []
loaded_facts.each do |loaded_fact|
searched_facts << SearchedFact.new(loaded_fact.name, loaded_fact.klass, '', loaded_fact.type)
end
searched_facts
end
def search_for_facts(query, loaded_facts)
resolvable_fact_list = []
query = query.to_s
query_tokens = query.end_with?('.*') ? [query] : query.split('.')
size = query_tokens.size
# Try to match the most specific query_tokens to the least, returning the first match
size.times do |i|
query_token_range = 0..size - i - 1
query_fact = query_tokens[query_token_range].join('.')
resolvable_fact_list = get_facts_matching_tokens(query_tokens, query_fact, loaded_facts)
return resolvable_fact_list if resolvable_fact_list.any?
end
resolvable_fact_list << SearchedFact.new(query, nil, query, :nil) if resolvable_fact_list.empty?
resolvable_fact_list
end
def get_facts_matching_tokens(query_tokens, query_fact, loaded_facts)
resolvable_fact_list = []
loaded_facts.each do |loaded_fact|
next unless found_fact?(loaded_fact.name, query_fact)
searched_fact = construct_loaded_fact(query_tokens, loaded_fact)
resolvable_fact_list << searched_fact
end
@log.debug "List of resolvable facts: #{resolvable_fact_list.inspect}" if resolvable_fact_list.any?
resolvable_fact_list
end
def found_fact?(fact_name, query_fact)
# This is the case where the fact_name contains a wildcard like
# blockdevice_.*_model and we're querying for the legacy fact
# specifically using 'blockdevice_sba_model' and we don't want the query
# 'blockdevice.sba.model' to match
fact_with_wildcard = fact_name.include?('.*') && !query_fact.include?('.')
if fact_with_wildcard
# fact_name contains wildcard, so we're intentially not escaping.
query_fact.match("^#{fact_name}$")
else
processed_equery_fact = query_fact.gsub('\\', '\\\\\\\\')
# Must escape metacharacters (like dots) to ensure the correct fact is found
fact_name.match("^#{Regexp.escape(processed_equery_fact)}($|\\.)")
end
end
def construct_loaded_fact(query_tokens, loaded_fact)
user_query = @query_list.any? ? query_tokens.join('.') : ''
fact_name = loaded_fact.name.to_s
klass_name = loaded_fact.klass
type = loaded_fact.type
sf = SearchedFact.new(fact_name, klass_name, user_query, type)
sf.file = loaded_fact.file
sf
end
end
end
end