class Tina4::GraphQLParser
def advance
def advance tok = @tokens[@pos] @pos += 1 tok end
def current
def current @tokens[@pos] end
def expect(type, value = nil)
def expect(type, value = nil) tok = current if tok.nil? raise GraphQLError, "Unexpected end of query, expected #{type} #{value}" end if tok.type != type || (value && tok.value != value) raise GraphQLError, "Expected #{type} '#{value}' at position #{tok.pos}, got #{tok.type} '#{tok.value}'" end advance end
def initialize(source)
def initialize(source) @source = source @tokens = tokenize(source) @pos = 0 end
def match(type, value = nil)
def match(type, value = nil) tok = current return nil unless tok return nil unless tok.type == type return nil if value && tok.value != value advance end
def parse
def parse document = { kind: :document, definitions: [] } while current skip(:comma) break unless current document[:definitions] << parse_definition end document end
def parse_arguments
def parse_arguments expect(:punct, "(") args = {} until current&.value == ")" skip(:comma) break if current&.value == ")" arg_name = expect(:name).value expect(:punct, ":") args[arg_name] = parse_value end expect(:punct, ")") args end
def parse_definition
def parse_definition tok = current if tok.nil? raise GraphQLError, "Unexpected end of input" end if tok.type == :keyword && tok.value == "fragment" return parse_fragment end if tok.type == :keyword && (tok.value == "query" || tok.value == "mutation") return parse_operation end # Shorthand query (just a selection set) if tok.type == :punct && tok.value == "{" return { kind: :operation, operation: :query, name: nil, variables: [], selection_set: parse_selection_set } end raise GraphQLError, "Unexpected token '#{tok.value}' at position #{tok.pos}" end
def parse_field
def parse_field name_tok = expect(:name) field_name = name_tok.value alias_name = nil # Check for alias: alias: fieldName if current&.value == ":" advance alias_name = field_name field_name = expect(:name).value end arguments = {} if current&.value == "(" arguments = parse_arguments end selection_set = nil if current&.value == "{" selection_set = parse_selection_set end { kind: :field, name: field_name, alias: alias_name, arguments: arguments, selection_set: selection_set } end
def parse_fragment
def parse_fragment expect(:keyword, "fragment") name = expect(:name).value expect(:keyword, "on") type_name = expect(:name).value selection_set = parse_selection_set { kind: :fragment, name: name, on: type_name, selection_set: selection_set } end
def parse_fragment_spread
def parse_fragment_spread expect(:spread) if current&.type == :keyword && current&.value == "on" # Inline fragment advance type_name = expect(:name).value selection_set = parse_selection_set { kind: :inline_fragment, on: type_name, selection_set: selection_set } else name = expect(:name).value { kind: :fragment_spread, name: name } end end
def parse_list_value
def parse_list_value expect(:punct, "[") items = [] until current&.value == "]" skip(:comma) break if current&.value == "]" items << parse_value end expect(:punct, "]") items end
def parse_object_value
def parse_object_value expect(:punct, "{") obj = {} until current&.value == "}" skip(:comma) break if current&.value == "}" key = expect(:name).value expect(:punct, ":") obj[key] = parse_value end expect(:punct, "}") obj end
def parse_operation
def parse_operation op = advance.value.to_sym # :query or :mutation name = match(:name)&.value variables = [] if current&.value == "(" variables = parse_variable_definitions end selection_set = parse_selection_set { kind: :operation, operation: op, name: name, variables: variables, selection_set: selection_set } end
def parse_selection_set
def parse_selection_set expect(:punct, "{") selections = [] until current&.value == "}" skip(:comma) break if current&.value == "}" if current&.type == :spread selections << parse_fragment_spread else selections << parse_field end end expect(:punct, "}") selections end
def parse_type_ref
def parse_type_ref if match(:punct, "[") inner = parse_type_ref expect(:punct, "]") type_str = "[#{inner}]" else type_str = expect(:name).value end type_str += "!" if match(:punct, "!") type_str end
def parse_value
def parse_value tok = current case tok.type when :string advance tok.value when :number advance tok.value.include?(".") ? tok.value.to_f : tok.value.to_i when :keyword advance case tok.value when "true" then true when "false" then false when "null" then nil else tok.value end when :name # Enum value advance tok.value when :punct if tok.value == "[" parse_list_value elsif tok.value == "{" parse_object_value elsif tok.value == "$" advance { kind: :variable, name: expect(:name).value } else raise GraphQLError, "Unexpected '#{tok.value}' in value at position #{tok.pos}" end else raise GraphQLError, "Unexpected token type #{tok.type} at position #{tok.pos}" end end
def parse_variable_definitions
def parse_variable_definitions expect(:punct, "(") vars = [] until current&.value == ")" skip(:comma) break if current&.value == ")" expect(:punct, "$") vname = expect(:name).value expect(:punct, ":") vtype = parse_type_ref default = nil if match(:punct, "=") default = parse_value end vars << { name: vname, type: vtype, default: default } end expect(:punct, ")") vars end
def peek(offset = 0)
def peek(offset = 0) @tokens[@pos + offset] end
def read_number(src, i)
def read_number(src, i) start = i i += 1 if src[i] == "-" i += 1 while i < src.length && src[i] =~ /[\d.eE+\-]/ [src[start...i], i] end
def read_string(src, i)
def read_string(src, i) i += 1 # skip opening quote str = "" while i < src.length && src[i] != '"' if src[i] == "\\" i += 1 case src[i] when "n" then str << "\n" when "t" then str << "\t" when '"' then str << '"' when "\\" then str << "\\" else str << src[i].to_s end else str << src[i] end i += 1 end i += 1 # skip closing quote [str, i] end
def skip(type, value = nil)
def skip(type, value = nil) match(type, value) while current && current.type == type && (value.nil? || current.value == value) end
def tokenize(src)
def tokenize(src) tokens = [] i = 0 while i < src.length ch = src[i] # Skip whitespace if ch =~ /\s/ i += 1 next end # Skip comments if ch == "#" i += 1 while i < src.length && src[i] != "\n" next end # Punctuation if "{}()[]!:=@$,".include?(ch) tokens << Token.new(:punct, ch, i) i += 1 next end # Spread operator if ch == "." && src[i + 1] == "." && src[i + 2] == "." tokens << Token.new(:spread, "...", i) i += 3 next end # String if ch == '"' str, i = read_string(src, i) tokens << Token.new(:string, str, i) next end # Number if ch =~ /[\d\-]/ num, i = read_number(src, i) tokens << Token.new(:number, num, i) next end # Name / keyword if ch =~ /[a-zA-Z_]/ name = "" while i < src.length && src[i] =~ /[a-zA-Z0-9_]/ name << src[i] i += 1 end type = KEYWORDS.include?(name) ? :keyword : :name tokens << Token.new(type, name, i - name.length) next end i += 1 # skip unknown end tokens end