class Priloo::RecursiveInjector

Constants

PreloadedValue

Public Instance Methods

inject(list, *path) click to toggle source
# File lib/priloo/recursive_injector.rb, line 5
def inject(list, *path)
    inject_deep(list.to_a, canonicalize(path))
    list
end

Private Instance Methods

canonicalize(path) click to toggle source

Transform a user-friendly path into a canonicalized form more suitable to further processing (idempotent)

User-friendly: [:a, :b, {c: [:d], b: :x}] Canonicalized: {:a=>{}, :b=>{:x=>{}}, :c=>{:d=>{}}}

# File lib/priloo/recursive_injector.rb, line 167
def canonicalize(path)
    case path
    when Array then path.reduce({}) { |a, p| a.merge(canonicalize(p)) }
    when Hash then path.map { |k, v| [k.to_sym, canonicalize(v)] }.to_h
    else { path.to_sym => {} }
    end
end
find_preloader(item, property) click to toggle source

Find a preloader for any object

# File lib/priloo/recursive_injector.rb, line 151
def find_preloader(item, property)
    return Preloaders::NilPreloader.instance if item.nil?

    method = :"#{property}__priloo__"
    preloader = item.send(method) if item.respond_to? method
    preloader ||= Preloaders::CollectionPreloader.instance if property == :__each__ && item.is_a?(Enumerable)
    preloader ||= Preloaders::NavigatingPreloader.new(property)
    raise "Cannot find any preloader for property '#{property}' of #{item}" unless preloader

    preloader
end
inject_deep(list, paths) click to toggle source

Preload dependencies at top level and recursively preload next levels

# File lib/priloo/recursive_injector.rb, line 115
def inject_deep(list, paths)
    resolved_paths, preloaders = resolve_dependencies(list, paths)

    resolved_paths.each_with_index do |(_key, next_level), key_idx|
        # We preload the key for every item
        preloaded_values = preload_single_key(list, preloaders[key_idx])

        # We recursively preload the next level
        preload_next_level(preloaded_values, next_level)
    end

    list
end
inject_deep_flat(list, remaining_path, multiplicity) click to toggle source

Little wrapper for 'inject_deep' implementing support for list-of-lists. The input list will be flattened 'multiplicity' times.

1- Flatten the input 'list' such that it can be handled by 'inject_deep' 2- Call 'inject_deep' 3- Un-flatten the preloaded output

# File lib/priloo/recursive_injector.rb, line 18
def inject_deep_flat(list, remaining_path, multiplicity)
    return inject_deep(list, remaining_path) if multiplicity == 0

    flat_input = list.flat_map { |v| v.nil? ? [] : v }
    flat_output = inject_deep_flat(flat_input, remaining_path, multiplicity - 1)
    item_index = -1
    list.map { |v| v&.map { flat_output[item_index += 1] } }
end
map_by_group(list, group_by: proc { nil }, map_to: proc { |_g, v| v }) click to toggle source
# File lib/priloo/recursive_injector.rb, line 129
def map_by_group(list, group_by: proc { nil }, map_to: proc { |_g, v| v })
    output = Array.new(list.size)

    list.each_with_index.group_by { |item, _idx| group_by[item] }
        .each do |group_key, input_items|
            input_items_values = input_items.map(&:first)
            result = map_to[group_key, input_items_values]

            raise 'Mapper should return same number of rows' unless result.size == input_items.size

            result.each_with_index do |result_item, result_idx|
                input_item = input_items[result_idx]
                input_item_idx = input_item.last

                output[input_item_idx] = result_item
            end
        end

    output
end
preload_items(preloader, items) click to toggle source
# File lib/priloo/recursive_injector.rb, line 84
def preload_items(preloader, items)
    map_by_group items,
        group_by: ->(x) { preloader.injected?(x) },
        map_to: ->(injected, filtered_items) {
            next filtered_items.map { |x| preloader.extract(x) } if injected

            preloader.preload(filtered_items).tap do |preloaded_values|
                filtered_items.each_with_index do |item, index|
                    preloader.inject(item, preloaded_values[index])
                end
            end
        }
end
preload_next_level(preloaded_values, next_level) click to toggle source
# File lib/priloo/recursive_injector.rb, line 98
def preload_next_level(preloaded_values, next_level)
    next_values = Array.new(preloaded_values.size)

    preloaded_values.group_by(&:multiplicity)
                    .flat_map do |multiplicity, grouped_preloaded_values|
        values = grouped_preloaded_values.map(&:value)
        results = inject_deep_flat(values, next_level, multiplicity)

        results.each_with_index do |result, result_index|
            next_values[grouped_preloaded_values[result_index].index] = result
        end
    end

    next_values
end
preload_single_key(list, preloaders) click to toggle source
# File lib/priloo/recursive_injector.rb, line 72
def preload_single_key(list, preloaders)
    list.each_with_index
        .group_by { |_item, item_idx| preloaders[item_idx].merge_key }
        .flat_map do |_merge_key, indexed_items|
            preloader = preloaders[indexed_items.first.last]
            items = indexed_items.map(&:first)
            preload_items(preloader, items).each_with_index.map do |value, value_idx|
                PreloadedValue.new(preloader.multiplicity, indexed_items[value_idx][1], value)
            end
        end
end
resolve_dependencies(list, paths) click to toggle source

Extract the necessary preloads & resolve their dependencies.

Return:

  • A new 'resolved_paths' including all recursively resolved dependencies, ordered appropriately.

  • A two-level list of preloaders[item_index] (every key/item couple gets its own preloader)

# File lib/priloo/recursive_injector.rb, line 32
def resolve_dependencies(list, paths)
    dependencies = {}
    preloaders = {}
    undiscovered = paths.keys

    until undiscovered.empty?
        undiscovered.each do |dep|
            dependencies[dep] = Set.new
            preloaders[dep] = list.map { |item| find_preloader(item, dep) }

            deps = preloaders[dep].uniq(&:merge_key)
                                  .reject { |preloader| preloader.is_a?(Preloaders::NilPreloader) }
                                  .map { |p| canonicalize(p.dependencies) }
                                  .uniq

            if deps.size > 1
                # This is a limitation of the current implementation, and at some point we will have to
                # fix it otherwise it could prevent some things to be done.
                raise NotImplementedError, 'Different dependencies at the same level are not supported'
            end

            deps.each do |d|
                dependencies[dep] += d.keys
                paths = paths.deep_merge(d)
            end
        end

        undiscovered = paths.keys - dependencies.keys
    end

    order = Dependencies.resolve(dependencies)

    resolved_paths = order.map { |key| [key, paths[key]] }.to_h
    preloaders = order.map { |key| preloaders[key] }

    [resolved_paths, preloaders]
end