/ lib / graphlyte / document.rb
document.rb
  1  # frozen_string_literal: true
  2  
  3  require 'forwardable'
  4  
  5  require_relative './syntax'
  6  require_relative './data'
  7  require_relative './serializer'
  8  require_relative './refinements/string_refinement'
  9  require_relative './editors/with_variables'
 10  
 11  module Graphlyte
 12    # The representation of a GraphQL document.
 13    #
 14    # Documents can have multiple definitions, which can
 15    # be queries, mutations, subscriptions (operations) or fragments.
 16    #
 17    # During execution, only one operation can be executed.
 18    class Document < Graphlyte::Data
 19      using Graphlyte::Refinements::StringRefinement
 20      extend Forwardable
 21  
 22      attr_accessor :definitions, :variables, :schema
 23  
 24      def_delegators :@definitions, :length, :empty?
 25  
 26      def initialize(**kwargs)
 27        super
 28        @definitions ||= []
 29        @variables ||= {}
 30        @var_name_counter = @variables.size + 1
 31      end
 32  
 33      def +(other)
 34        return dup unless other
 35  
 36        other = other.dup
 37        doc = dup
 38  
 39        defs = doc.definitions + other.definitions
 40        vars = doc.variables.merge(other.variables) # TODO: detect conflicts?
 41  
 42        self.class.new(definitions: defs, vars: vars)
 43      end
 44  
 45      def eql?(other)
 46        other.is_a?(self.class) && other.fragments == fragments && other.operations == operations
 47      end
 48  
 49      alias == eql?
 50  
 51      def define(dfn)
 52        @definitions << dfn
 53      end
 54  
 55      def add_fragments(frags)
 56        current = fragments
 57  
 58        frags.each do |frag|
 59          @definitions << frag unless current[frag.name]
 60        end
 61      end
 62  
 63      def declare(var)
 64        if var.name.nil?
 65          var.name = "var#{@var_name_counter}"
 66          @var_name_counter += 1
 67        end
 68  
 69        parser = Graphlyte::Parser.new(tokens: Graphlyte::Lexer.lex(var.type))
 70        parsed_type = parser.type_name! if var.type
 71        current_def = @variables[var.name]
 72  
 73        if current_def && current_def.type != parsed_type
 74          msg = "Cannot re-declare #{var.name} at different types. #{current_def.type} != #{var.type}"
 75          raise ArgumentError, msg
 76        end
 77  
 78        @variables[var.name] ||= Graphlyte::Syntax::VariableDefinition.new(
 79          variable: var.name,
 80          type: parsed_type
 81        )
 82  
 83        Syntax::VariableReference.new(var.name, parsed_type)
 84      end
 85  
 86      def fragments
 87        definitions.select { _1.is_a?(Graphlyte::Syntax::Fragment) }.to_h { [_1.name, _1] }
 88      end
 89  
 90      def operations
 91        @definitions.select { _1.is_a?(Graphlyte::Syntax::Operation) }.to_h { [_1.name, _1] }
 92      end
 93  
 94      def executable?
 95        @definitions.all?(&:executable?)
 96      end
 97  
 98      def to_s
 99        buff = []
100        write(buff)
101  
102        buff.join
103      end
104  
105      # More efficient for writing to files or streams - avoids building up the full string.
106      def write(io)
107        Graphlyte::Serializer.new(io).dump_definitions(definitions)
108      end
109  
110      # Return this document as a JSON request body, suitable for posting to a server.
111      def request_body(operation = nil, **variables)
112        if operation.nil? && operations.size != 1
113          raise ArgumentError, 'Operation name is required when the document contains multiple operations'
114        end
115  
116        variables.transform_keys! { _1.to_s.camelize }
117  
118        doc = Editors::WithVariables.new(schema, operation, variables).edit(dup)
119  
120        {
121          query: doc.to_s,
122          variables: variables,
123          operation: operation
124        }.compact.to_json
125      end
126  
127      def variable_references
128        Editors::CollectVariableReferences.new.edit(self)[Syntax::Operation]
129      end
130    end
131  end