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