# frozen_string_literal: true module GraphQL class Schema class Visibility class Visit def initialize(schema, &visit_block) @schema = schema @late_bound_types = nil @unvisited_types = nil # These accumulate between calls to prevent re-visiting the same types @visited_types = Set.new.compare_by_identity @visited_directives = Set.new.compare_by_identity @visit_block = visit_block end def entry_point_types ept = [ @schema.query, @schema.mutation, @schema.subscription, *@schema.introspection_system.types.values, *@schema.orphan_types, ] ept.compact! ept end def entry_point_directives @schema.directives.values end def visit_each(types: entry_point_types, directives: entry_point_directives) @unvisited_types && raise("Can't re-enter `visit_each` on this Visit (another visit is already in progress)") @unvisited_types = types @late_bound_types = [] directives_to_visit = directives while !@unvisited_types.empty? || !@late_bound_types.empty? while (type = @unvisited_types.pop) if @visited_types.add?(type) && @visit_block.call(type) directives_to_visit.concat(type.directives) case type.kind.name when "OBJECT", "INTERFACE" type.interface_type_memberships.each do |itm| append_unvisited_type(type, itm.abstract_type) end if type.kind.interface? type.orphan_types.each do |orphan_type| append_unvisited_type(type, orphan_type) end end type.all_field_definitions.each do |field| field.ensure_loaded if @visit_block.call(field) directives_to_visit.concat(field.directives) append_unvisited_type(type, field.type.unwrap) field.all_argument_definitions.each do |argument| if @visit_block.call(argument) directives_to_visit.concat(argument.directives) append_unvisited_type(field, argument.type.unwrap) end end end end when "INPUT_OBJECT" type.all_argument_definitions.each do |argument| if @visit_block.call(argument) directives_to_visit.concat(argument.directives) append_unvisited_type(type, argument.type.unwrap) end end when "UNION" type.type_memberships.each do |tm| append_unvisited_type(type, tm.object_type) end when "ENUM" type.all_enum_value_definitions.each do |val| if @visit_block.call(val) directives_to_visit.concat(val.directives) end end when "SCALAR" # pass -- nothing else to visit else raise "Invariant: unhandled type kind: #{type.kind.inspect}" end end end directives_to_visit.each do |dir| dir_class = dir.is_a?(Class) ? dir : dir.class if @visited_directives.add?(dir_class) && @visit_block.call(dir_class) dir_class.all_argument_definitions.each do |arg_defn| if @visit_block.call(arg_defn) directives_to_visit.concat(arg_defn.directives) append_unvisited_type(dir_class, arg_defn.type.unwrap) end end end end missed_late_types_streak = 0 while (owner, late_type = @late_bound_types.shift) if (late_type.is_a?(String) && (type = Member::BuildType.constantize(type))) || (late_type.is_a?(LateBoundType) && (type = @visited_types.find { |t| t.graphql_name == late_type.graphql_name })) missed_late_types_streak = 0 # might succeed next round update_type_owner(owner, type) append_unvisited_type(owner, type) else # Didn't find it -- keep trying missed_late_types_streak += 1 @late_bound_types << [owner, late_type] if missed_late_types_streak == @late_bound_types.size raise UnresolvedLateBoundTypeError.new(type: late_type) end end end end @unvisited_types = nil nil end private def append_unvisited_type(owner, type) if type.is_a?(LateBoundType) || type.is_a?(String) @late_bound_types << [owner, type] else @unvisited_types << type end end def update_type_owner(owner, type) case owner when Module if owner.kind.union? owner.assign_type_membership_object_type(type) elsif type.kind.interface? new_interfaces = [] owner.interfaces.each do |int_t| if int_t.is_a?(String) && int_t == type.graphql_name new_interfaces << type elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name new_interfaces << type else # Don't re-add proper interface definitions, # they were probably already added, maybe with options. end end owner.implements(*new_interfaces) new_interfaces.each do |int| pt = @possible_types[int] ||= [] if !pt.include?(owner) && owner.is_a?(Class) pt << owner end int.interfaces.each do |indirect_int| if indirect_int.is_a?(LateBoundType) && (indirect_int_type = get_type(indirect_int.graphql_name)) # rubocop:disable Development/ContextIsPassedCop update_type_owner(owner, indirect_int_type) end end end end when GraphQL::Schema::Argument, GraphQL::Schema::Field orig_type = owner.type # Apply list/non-null wrapper as needed if orig_type.respond_to?(:of_type) transforms = [] while (orig_type.respond_to?(:of_type)) if orig_type.kind.non_null? transforms << :to_non_null_type elsif orig_type.kind.list? transforms << :to_list_type else raise "Invariant: :of_type isn't non-null or list" end orig_type = orig_type.of_type end transforms.reverse_each { |t| type = type.public_send(t) } end owner.type = type else raise "Unexpected update: #{owner.inspect} #{type.inspect}" end end end end end end