mirror of
https://github.com/inspec/inspec
synced 2024-11-10 07:04:15 +00:00
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:
commit
02f97ac845
4 changed files with 121 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue