class FilterTable::Factory

Constants

CustomPropertyType

Public Class Methods

new() click to toggle source
# File lib/inspec/utils/filter.rb, line 294
def initialize
  @filter_methods = %i{where entries raw_data}
  @custom_properties = {}
  register_custom_matcher(:exist?) { |table| !table.raw_data.empty? }
  register_custom_property(:count) { |table|  table.raw_data.count }

  @resource = nil # TODO: this variable is never initialized
end

Public Instance Methods

add(property_name, opts = {}, &property_implementation)
add_accessor(method_name)
connect(resource_class, raw_data_fetcher_method_name)
install_filter_methods_on_resource(resource_class, raw_data_fetcher_method_name) click to toggle source
# File lib/inspec/utils/filter.rb, line 303
def install_filter_methods_on_resource(resource_class, raw_data_fetcher_method_name) # rubocop: disable Metrics/AbcSize, Metrics/MethodLength
  # A context in which you can access the fields as accessors
  non_block_struct_fields = @custom_properties.values.reject(&:block).map(&:field_name)
  unless non_block_struct_fields.empty?
    row_eval_context_type = Struct.new(*non_block_struct_fields.map(&:to_sym)) do
      attr_accessor :criteria_string
      attr_accessor :filter_table
      def to_s
        @criteria_string || super
      end
    end
  end

  properties_to_define = @custom_properties.map do |method_name, custom_property_structure|
    { method_name: method_name, method_body: create_custom_property_body(custom_property_structure) }
  end

  # Define the filter table subclass
  custom_properties = @custom_properties # We need a local var, not an instance var, for a closure below
  table_class = Class.new(Table) do
    # Install each custom property onto the FilterTable subclass
    properties_to_define.each do |property_info|
      define_method property_info[:method_name], &property_info[:method_body]
    end

    define_method :custom_properties_schema do
      custom_properties
    end

    # Install a method that can wrap all the fields into a context with accessors
    define_method :create_eval_context_for_row do |row_as_hash, criteria_string = ""|
      return row_eval_context_type.new if row_as_hash.nil?

      context = row_eval_context_type.new(*non_block_struct_fields.map { |field| row_as_hash[field] })
      context.criteria_string = criteria_string
      context.filter_table = self
      context
    end
  end

  # Now that the table class is defined and the row eval context struct is defined,
  # extend the row eval context struct to support triggering population of lazy fields
  # in where blocks. To do that, we'll need a reference to the table (which
  # knows which fields are populated, and how to populate them) and we'll need to
  # override the getter method for each lazy field, so it will trigger
  # population if needed.  Keep in mind we don't have to adjust the constructor
  # args of the row struct; also the Struct class will already have provided
  # a setter for each field.
  @custom_properties.values.each do |property_info|
    next unless property_info.opts[:lazy]

    field_name = property_info.field_name.to_sym
    row_eval_context_type.send(:define_method, field_name) do
      unless filter_table.field_populated?(field_name)
        filter_table.populate_lazy_field(field_name, NoCriteriaProvided) # No access to criteria here
        # OK, the underlying raw data has the value in the first row
        # (because we would trigger population only on the first row)
        # We could just return the value, but we need to set it on this Struct in case it is referenced multiple times
        # in the where block.
        self[field_name] = filter_table.raw_data[0][field_name]
      end
      # Now return the value using the Struct getter, whether newly populated or not
      self[field_name]
    end
  end

  # Define all access methods with the parent resource
  # These methods will be configured to return an `ExceptionCatcher` object
  # that will always return the original exception, but only when called
  # upon. This will allow method chains in `describe` statements to pass the
  # `instance_eval` when loaded and only throw-and-catch the exception when
  # the tests are run.
  methods_to_install_on_resource_class = @filter_methods + @custom_properties.keys
  methods_to_install_on_resource_class.each do |method_name|
    resource_class.send(:define_method, method_name) do |*args, &block|

      # self here is the resource instance
      filter_table_instance = table_class.new(self, send(raw_data_fetcher_method_name), " with")
      filter_table_instance.send(method_name, *args, &block)
    rescue Inspec::Exceptions::ResourceFailed, Inspec::Exceptions::ResourceSkipped => e
      FilterTable::ExceptionCatcher.new(resource_class, e)

    end
  end
end
Also aliased as: connect
register_column(property_name, opts = {}, &property_implementation)
register_custom_matcher(property_name, opts = {}, &property_implementation)
register_custom_property(property_name, opts = {}, &property_implementation) click to toggle source
# File lib/inspec/utils/filter.rb, line 408
def register_custom_property(property_name, opts = {}, &property_implementation)
  if property_name.nil?
    # TODO: @resource is never initialized
    throw RuntimeError, "Called filter.add for resource #{@resource} with method name nil!"
  end

  if @custom_properties.key?(property_name.to_sym)
    # TODO: issue deprecation warning?
  else
    @custom_properties[property_name.to_sym] =
      CustomPropertyType.new(opts[:field] || property_name, property_implementation, opts)
  end
  self
end
register_filter_method(method_name) click to toggle source

TODO: This should almost certainly be privatized. Every FilterTable client should get :entries and :where; InSpec core resources do not define anything else, other than azure_generic_resource, which is likely a mis-use.

# File lib/inspec/utils/filter.rb, line 393
def register_filter_method(method_name)
  if method_name.nil?
    # TODO: @resource is never initialized
    throw RuntimeError, "Called filter.add_accessor for resource #{@resource} with method name nil!"
  end
  if @filter_methods.include? method_name.to_sym
    # TODO: issue deprecation warning?
  else
    @filter_methods.push(method_name.to_sym)
  end
  self
end
Also aliased as: add_accessor
to_s() click to toggle source
Calls superclass method
# File lib/inspec/utils/filter.rb, line 310
def to_s
  @criteria_string || super
end

Private Instance Methods

create_custom_property_body(custom_property_struct) click to toggle source

This provides the implementation for methods requested using register_custom_property(:some_method_name, opts, &block) Some usage in the wild involves people passing a desired value to the generated method, like:

things.ids(23)

I'm calling this the 'filter_criterion_value'. I speculate that a default value is provided here so that users can meaningfully query for nil.

# File lib/inspec/utils/filter.rb, line 435
def create_custom_property_body(custom_property_struct)
  if !custom_property_struct.block.nil?
    # If the custom method provided its own block, rely on it.
    lambda do |filter_criteria_value = NoCriteriaProvided|
      # Here self is an instance of the FilterTable subclass that wraps the raw data.
      # Call the block with two args - the table instance, and any filter criteria value.
      custom_property_struct.block.call(self, filter_criteria_value)
    end
  else
    # No block definition, so the property was registered using (field: :some_field)
    # This does however support passing a block to this method, and filtering using it, like Enumerable#select.
    lambda do |filter_criteria_value = NoCriteriaProvided, &cond_block|
      if filter_criteria_value == NoCriteriaProvided && !block_given?
        # No second-order block given.  Just return an array of the values in the selected column.
        result = where(nil)
        if custom_property_struct.opts[:lazy]
          result.populate_lazy_field(custom_property_struct.field_name, filter_criteria_value)
        end
        result = where(nil).get_column_values(custom_property_struct.field_name) # TODO: the where(nil). is likely unneeded
        result = result.flatten.uniq.compact if custom_property_struct.opts[:style] == :simple
        result
      else
        # A secondary block was provided.  Rely on where() to execute the block, while also filtering on any single value
        # Suspected bug: if filter_criteria_value == NoCriteriaProvided, this is unlikely to match - see hash condition handling in where() above.
        where(custom_property_struct.field_name => filter_criteria_value, &cond_block)
      end
    end
  end
end