# frozen_string_literal: true module GraphQL module Execution class Interpreter class Runtime module GraphQLResult def initialize(result_name, result_type, application_value, parent_result, is_non_null_in_parent, selections, is_eager) @graphql_parent = parent_result @graphql_application_value = application_value @graphql_result_type = result_type if parent_result && parent_result.graphql_dead @graphql_dead = true end @graphql_result_name = result_name @graphql_is_non_null_in_parent = is_non_null_in_parent # Jump through some hoops to avoid creating this duplicate storage if at all possible. @graphql_metadata = nil @graphql_selections = selections @graphql_is_eager = is_eager end def path @path ||= build_path([]) end def build_path(path_array) graphql_result_name && path_array.unshift(graphql_result_name) @graphql_parent ? @graphql_parent.build_path(path_array) : path_array end attr_accessor :graphql_dead attr_reader :graphql_parent, :graphql_result_name, :graphql_is_non_null_in_parent, :graphql_application_value, :graphql_result_type, :graphql_selections, :graphql_is_eager # @return [Hash] Plain-Ruby result data (`@graphql_metadata` contains Result wrapper objects) attr_accessor :graphql_result_data end class GraphQLResultHash def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent, _selections, _is_eager) super @graphql_result_data = {} end include GraphQLResult attr_accessor :graphql_merged_into def set_leaf(key, value) # This is a hack. # Basically, this object is merged into the root-level result at some point. # But the problem is, some lazies are created whose closures retain reference to _this_ # object. When those lazies are resolved, they cause an update to this object. # # In order to return a proper top-level result, we have to update that top-level result object. # In order to return a proper partial result (eg, for a directive), we have to update this object, too. # Yowza. if (t = @graphql_merged_into) t.set_leaf(key, value) end @graphql_result_data[key] = value # keep this up-to-date if it's been initialized @graphql_metadata && @graphql_metadata[key] = value value end def set_child_result(key, value) if (t = @graphql_merged_into) t.set_child_result(key, value) end @graphql_result_data[key] = value.graphql_result_data # If we encounter some part of this response that requires metadata tracking, # then create the metadata hash if necessary. It will be kept up-to-date after this. (@graphql_metadata ||= @graphql_result_data.dup)[key] = value value end def delete(key) @graphql_metadata && @graphql_metadata.delete(key) @graphql_result_data.delete(key) end def each (@graphql_metadata || @graphql_result_data).each { |k, v| yield(k, v) } end def values (@graphql_metadata || @graphql_result_data).values end def key?(k) @graphql_result_data.key?(k) end def [](k) (@graphql_metadata || @graphql_result_data)[k] end def merge_into(into_result) self.each do |key, value| case value when GraphQLResultHash next_into = into_result[key] if next_into value.merge_into(next_into) else into_result.set_child_result(key, value) end when GraphQLResultArray # There's no special handling of arrays because currently, there's no way to split the execution # of a list over several concurrent flows. into_result.set_child_result(key, value) else # We have to assume that, since this passed the `fields_will_merge` selection, # that the old and new values are the same. into_result.set_leaf(key, value) end end @graphql_merged_into = into_result end end class GraphQLResultArray include GraphQLResult def initialize(_result_name, _result_type, _application_value, _parent_result, _is_non_null_in_parent, _selections, _is_eager) super @graphql_result_data = [] end def graphql_skip_at(index) # Mark this index as dead. It's tricky because some indices may already be storing # `Lazy`s. So the runtime is still holding indexes _before_ skipping, # this object has to coordinate incoming writes to account for any already-skipped indices. @skip_indices ||= [] @skip_indices << index offset_by = @skip_indices.count { |skipped_idx| skipped_idx < index} delete_at_index = index - offset_by @graphql_metadata && @graphql_metadata.delete_at(delete_at_index) @graphql_result_data.delete_at(delete_at_index) end def set_leaf(idx, value) if @skip_indices offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx } idx -= offset_by end @graphql_result_data[idx] = value @graphql_metadata && @graphql_metadata[idx] = value value end def set_child_result(idx, value) if @skip_indices offset_by = @skip_indices.count { |skipped_idx| skipped_idx < idx } idx -= offset_by end @graphql_result_data[idx] = value.graphql_result_data # If we encounter some part of this response that requires metadata tracking, # then create the metadata hash if necessary. It will be kept up-to-date after this. (@graphql_metadata ||= @graphql_result_data.dup)[idx] = value value end def values (@graphql_metadata || @graphql_result_data) end end end end end end