Merge pull request #5916 from inspec/RESOURCE-312-extend-filter-table

RESOURCE-312 extend filter table to lazy loading for resource instances
This commit is contained in:
Clinton Wolfe 2022-03-11 14:39:54 -05:00 committed by GitHub
commit 02f97ac845
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 12 deletions

View file

@ -388,6 +388,8 @@ In some cases, the raw data may require multiple actions to populate. For examp
Lazy loaded columns are absent in the raw data, until they are accessed (either by method-where, block-where, or a list property). When they are accessed, a user-provided Lambda is called, which populates one or more columns. FilterTable remembers which lazy columns have been populated, and will not call the lambda again.
If you know you want to access the resource instance in your lazy method callback, see `lazy_instance`.
### Declaring a lazy field
You declare a field to be lazy by providing an option, `lazy`, whose value is the lambda to be called.
@ -464,6 +466,30 @@ You can even have multiple lazy columns share an implementation; the first one t
Yes. Using `table.raw_data`, you could perform a column-at-once population. After the fetcher was called for the first row, all other rows would already be populated, so the fetcher would not be called again due to the no-clobber effect.
## `lazy_instance`
If you wish to do lazy loading but wish that you could use an instance method of the resource, you can do so using the `lazy_instance` property to set the callback.
```ruby
filter_table_config.register_column(
:colors,
field: :color,
lazy_instance: :make_it_red },
)
# instance, not class method
def make_it_red(row, condition, table)
row[:color] = :red
end
```
The method will be provided three arguments:
1. `row`. This is a Hash, the current row of the raw_data. You will likely need to examine this to find an ID value or other field that will act as a search key for your fetch. You are expected to add one or more entries to this hash, as a result of your fetch.
2. `condition`. In some cases, a condition (desired value) is provided; the semantics of this are up to you.
3. `table`. A reference to the FilterTable. You can use this to access other context - including the entire raw data (`table.raw_data`).
## Gotchas and Surprises
### Methods defined with `register_column` will change their return type based on their call pattern

View file

@ -114,6 +114,7 @@ module FilterTable
raise(ArgumentError, "'#{decorate_symbols(raw_field_name)}' is not a recognized criterion - expected one of #{decorate_symbols(list_fields).join(", ")}'") unless field?(raw_field_name)
populate_lazy_field(raw_field_name, desired_value) if is_field_lazy?(raw_field_name)
populate_lazy_instance_field(raw_field_name, desired_value) if is_field_lazy_instance?(raw_field_name)
new_criteria_string += " #{raw_field_name} == #{desired_value.inspect}"
filtered_raw_data = filter_raw_data(filtered_raw_data, raw_field_name, desired_value)
end
@ -188,6 +189,8 @@ module FilterTable
is_field ||= list_fields.include?(proposed_field.to_sym)
is_field ||= is_field_lazy?(proposed_field.to_s)
is_field ||= is_field_lazy?(proposed_field.to_sym)
is_field ||= is_field_lazy_instance?(proposed_field.to_s)
is_field ||= is_field_lazy_instance?(proposed_field.to_sym)
is_field
end
@ -210,6 +213,23 @@ module FilterTable
mark_lazy_field_populated(field_name)
end
def populate_lazy_instance_field(field_name, criterion)
return unless is_field_lazy_instance?(field_name)
return if field_populated?(field_name)
raw_data.each do |row|
next if row.key?(field_name) # skip row if pre-existing data is present
lazy_caller = callback_for_lazy_instance_field(field_name)
if lazy_caller.is_a?(Proc)
lazy_caller.call(row, criterion, resource_instance)
elsif lazy_caller.is_a?(Symbol)
resource_instance.send(lazy_caller, row, criterion, self)
end
end
mark_lazy_field_populated(field_name)
end
def is_field_lazy?(sought_field_name)
custom_properties_schema.values.any? do |property_struct|
sought_field_name == property_struct.field_name && \
@ -217,6 +237,13 @@ module FilterTable
end
end
def is_field_lazy_instance?(sought_field_name)
custom_properties_schema.values.any? do |property_struct|
sought_field_name == property_struct.field_name && \
property_struct.opts[:lazy_instance]
end
end
def callback_for_lazy_field(field_name)
return unless is_field_lazy?(field_name)
@ -225,6 +252,14 @@ module FilterTable
end.opts[:lazy]
end
def callback_for_lazy_instance_field(field_name)
return unless is_field_lazy_instance?(field_name)
custom_properties_schema.values.find do |property_struct|
property_struct.field_name == field_name
end.opts[:lazy_instance]
end
def field_populated?(field_name)
@populated_lazy_columns[field_name]
end
@ -349,12 +384,18 @@ module FilterTable
# 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]
next unless property_info.opts[:lazy] || property_info.opts[:lazy_instance]
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
if property_info.opts[:lazy]
filter_table.populate_lazy_field(field_name, NoCriteriaProvided)
end # No access to criteria here
if property_info.opts[:lazy_instance]
filter_table.populate_lazy_instance_field(field_name,
NoCriteriaProvided)
end
# 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
@ -449,7 +490,10 @@ module FilterTable
result = where(nil)
if custom_property_struct.opts[:lazy]
result.populate_lazy_field(custom_property_struct.field_name, filter_criteria_value)
elsif custom_property_struct.opts[:lazy_instance]
result.populate_lazy_instance_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

View file

@ -13,6 +13,8 @@ end
# lazy_2 populates with a constant symbol but encounters a collision
# lazy_3 increments on each call
# lazy_4 throws an exception on call
# lazy_5 increments on each call via a lazy_instance hook set with a lambda
# lazy_6 increments on each call via a lazy_instance hook set with a Symbol ref to an instance method
control '2370_where_block' do
desc 'When we call where as a block, lazy columns should load if referenced'
@ -24,14 +26,26 @@ control '2370_where_block' do
describe lazy_loader(fresh_data.call).where { lazy_3 == 1 } do
its('count') { should cmp 1 }
its('lazy_3s.first') { should cmp 1 }
its('resource.lazy_3_call_count') { should == 3 }
its('resource.lazy_3_call_count') { should == 3 }
end
describe lazy_loader(fresh_data.call).where { lazy_5 == 1 } do
its('count') { should cmp 1 }
its('lazy_5s.first') { should cmp 1 }
its('resource.lazy_5_call_count') { should == 3 }
end
describe lazy_loader(fresh_data.call).where { lazy_6 == 1 } do
its('count') { should cmp 1 }
its('lazy_6s.first') { should cmp 1 }
its('resource.lazy_6_call_count') { should == 3 }
end
end
control '2370_where_block_only_referenced' do
desc 'When we call where as a block, lazy columns should not load unless referenced'
describe lazy_loader(fresh_data.call).where { color == :red } do
[ :lazy_1, :lazy_2, :lazy_3, :lazy_4 ].each do |lazy_field|
[ :lazy_1, :lazy_2, :lazy_3, :lazy_4, :lazy_5, :lazy_6 ].each do |lazy_field|
its('raw_data.first.keys') { should_not include lazy_field }
end
end
@ -47,14 +61,26 @@ control '2370_where_method' do
describe lazy_loader(fresh_data.call).where(lazy_3: 1) do
its('count') { should cmp 1 }
its('lazy_3s.first') { should cmp 1 }
its('resource.lazy_3_call_count') { should == 3 }
its('resource.lazy_3_call_count') { should == 3 }
end
describe lazy_loader(fresh_data.call).where(lazy_5: 1) do
its('count') { should cmp 1 }
its('lazy_5s.first') { should cmp 1 }
its('resource.lazy_5_call_count') { should == 3 }
end
describe lazy_loader(fresh_data.call).where(lazy_6: 1) do
its('count') { should cmp 1 }
its('lazy_6s.first') { should cmp 1 }
its('resource.lazy_6_call_count') { should == 3 }
end
end
control '2370_where_method_only_referenced' do
desc 'When we call where as a method, lazy columns should not load unless referenced'
describe lazy_loader(fresh_data.call).where(color: :red) do
[ :lazy_1, :lazy_2, :lazy_3, :lazy_4 ].each do |lazy_field|
[ :lazy_1, :lazy_2, :lazy_3, :lazy_4, :lazy_5, :lazy_6 ].each do |lazy_field|
its('params.first.keys') { should_not include lazy_field }
end
end
@ -72,7 +98,7 @@ end
control '2370_no_side_populate' do
desc 'When we trigger a populate on one column, it should not trigger a populate on another column.'
describe lazy_loader(fresh_data.call).where( lazy_1: :lazy_1_loaded ) do
[ :lazy_2, :lazy_3, :lazy_4 ].each do |lazy_field|
[ :lazy_2, :lazy_3, :lazy_4, :lazy_5, :lazy_6 ].each do |lazy_field|
its('params.first.keys') { should_not include lazy_field }
end
end
@ -114,6 +140,8 @@ control '2370_no_rows' do
desc 'When the data has no rows, the lazy populator should not get called'
describe lazy_loader([]).where { lazy_3 } do
its('resource.lazy_3_call_count') { should be_zero }
its('resource.lazy_5_call_count') { should be_zero }
its('resource.lazy_6_call_count') { should be_zero }
end
end

View file

@ -3,22 +3,33 @@ class LazyLoader < Inspec.resource(1)
attr_reader :plain_data
attr_accessor :lazy_3_call_count
attr_accessor :lazy_5_call_count
attr_accessor :lazy_6_call_count
def initialize(provided_data)
@plain_data = provided_data
@lazy_3_call_count = 0
@lazy_5_call_count = 0
@lazy_6_call_count = 0
end
filter_table_generator = FilterTable.create
filter_table_generator.add_accessor(:where)
filter_table_generator.add_accessor(:where)
filter_table_generator.add_accessor(:entries)
filter_table_generator.add(:exists?) { |table| !table.entries.empty? }
filter_table_generator.add(:count) { |table| table.params.count }
filter_table_generator.add(:exists?) { |table| !table.entries.empty? }
filter_table_generator.add(:count) { |table| table.params.count }
filter_table_generator.add(:ids, field: :id)
filter_table_generator.add(:colors, field: :color)
filter_table_generator.add(:lazy_1s, field: :lazy_1, lazy: ->(r,c,t) { r[:lazy_1] = :lazy_1_loaded } )
filter_table_generator.add(:lazy_2s, field: :lazy_2, lazy: ->(r,c,t) { r[:lazy_2] =:lazy_2_loaded } )
filter_table_generator.add(:lazy_3s, field: :lazy_3, lazy: ->(r,c,t) { r[:lazy_3] = t.resource.lazy_3_call_count += 1 } )
filter_table_generator.add(:lazy_4s, field: :lazy_4, lazy: ->(r,c,t) { 1 / 0 } )
filter_table_generator.add(:lazy_4s, field: :lazy_4, lazy: ->(r,c,t) { 1 / 0 } )
filter_table_generator.add(:lazy_5s, field: :lazy_5, lazy_instance: ->(r,c,i) { r[:lazy_5] = i.lazy_5_call_count += 1 } )
filter_table_generator.add(:lazy_6s, field: :lazy_6, lazy_instance: :increment_lazy_6 )
filter_table_generator.connect(self, :plain_data)
end
def increment_lazy_6(row, _crit, _table)
# BUG: self here is different every time this is called, and appears not to be initialized
row[:lazy_6] = (@lazy_6_call_count += 1)
end
end