class ArSerializer::GraphQL::Parser

Attributes

chars[R]
operation_name[R]
query[R]
variables[R]

Public Class Methods

new(query, operation_name: nil, variables: {}) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 5
def initialize(query, operation_name: nil, variables: {})
  @query = query
  @operation_name = operation_name
  @variables = variables
  @chars = query.chars
end
parse(query, **option) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 12
def self.parse(query, **option)
  new(query, **option).parse
end

Public Instance Methods

parse() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 16
def parse
  definitions = []
  consume_blank
  loop do
    definition = parse_definition
    consume_blank
    consume_text ','
    consume_blank
    break unless definition
    definitions << definition
  end
  raise_expected_not_found 'definition or EOF' unless chars.empty?
  query = definitions.find do |definition|
    next unless definition[:type] == 'query'
    operation_name.nil? || operation_name == definition[:args].first
  end
  raise ParseError, 'empty query' unless query
  fragments = definitions.select { |definition| definition[:type] == 'fragment' }
  fragments_by_name = fragments.index_by { |frag| frag[:args].first }
  embed_fragment query[:fields], fragments_by_name
end

Private Instance Methods

consume_blank() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 50
def consume_blank
  loop do
    chars.shift while chars.first&.match?(/\s/)
    return unless consume_comment
  end
end
consume_comment() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 40
def consume_comment
  return false if chars.first != '#'
  until chars.blank?
    c = chars.first
    break if c == "\n"
    chars.shift
  end
  true
end
consume_text(s) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 57
def consume_text(s)
  return false unless chars.take(s.size).join == s
  chars.shift s.size
  true
end
consume_text!(s) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 63
def consume_text!(s)
  return if consume_text s
  raise_expected_not_found s.inspect
end
current_position_message() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 231
def current_position_message
  pos = query.size - chars.size
  code = query[[pos - 10, 0].max..pos + 10]
  line_num = 0
  query.each_line.with_index 1 do |l, i|
    line_num = i
    break if pos < l.size
    pos -= l.size
  end
  "at #{line_num}:#{pos} near #{code.inspect}"
end
embed_fragment(fields, fragments) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 243
def embed_fragment(fields, fragments)
  output = {}
  fields.each do |key, value|
    if value.is_a?(Hash) && (fragment_name = value[:fragment])
      fragment = fragments[fragment_name]
      extract_fragment fragment_name, fragments
      output.update fragment[:fields]
    else
      output[key] = value
      if (attrs = value[:attributes])
        value[:attributes] = embed_fragment attrs, fragments
      end
    end
  end
  output
end
extract_fragment(fragment_name, fragments) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 260
def extract_fragment(fragment_name, fragments)
  fragment = fragments[fragment_name]
  raise ParseError, "fragment named #{fragment_name.inspect} was not found" if fragment.nil?
  raise ParseError, "fragment circular definition detected in #{fragment_name.inspect}" if fragment[:state] == :start
  return if fragment[:state] == :done
  fragment[:state] = :start
  fragment[:fields] = embed_fragment fragment[:fields], fragments
  fragment[:state] = :done
end
parse_arg_fields() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 153
def parse_arg_fields
  result = {}
  loop do
    name = parse_name
    break unless name
    consume_blank
    consume_text! ':'
    consume_blank
    value = parse_arg_value
    if value == :none
      raise(
        ParseError,
        "expected hash value but nothing found #{current_position_message}"
      )
    end
    result[name] = value
    consume_blank
    consume_text ','
    consume_blank
  end
  result
end
parse_arg_value() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 93
def parse_arg_value
  case chars.first
  when '"'
    chars.shift
    s = ''
    loop do
      if chars.first == '\\'
        s << chars.shift
        s << chars.shift
      elsif chars.first == '"'
        break
      else
        s << chars.shift
      end
    end
    chars.shift
    unescape_string s
  when '['
    chars.shift
    result = []
    loop do
      consume_blank
      value = parse_arg_value
      consume_blank
      consume_text ','
      break if value == :none
      result << value
    end
    consume_text! ']'
    result
  when '{'
    chars.shift
    consume_blank
    result = parse_arg_fields
    consume_blank
    consume_text! '}'
    result
  when '$'
    chars.shift
    name = parse_name
    variables[name]
  when /[0-9+\-]/
    s = ''
    s << chars.shift while chars.first.match?(/[0-9.e+\-]/)
    s.match?(/\.|e/) ? s.to_f : s.to_i
  when /[a-zA-Z]/
    s = parse_name
    converts = { 'true' => true, 'false' => false, 'null' => nil }
    converts.key?(s) ? converts[s] : s
  else
    :none
  end
end
parse_args() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 176
def parse_args
  return unless consume_text '('
  consume_blank
  args = parse_arg_fields
  consume_blank
  consume_text! ')'
  args
end
parse_definition() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 216
def parse_definition
  type = parse_name
  consume_blank
  args_text = ''
  if type
    args_text << chars.shift while chars.first && chars.first != '{'
  end
  args = args_text.split(/[\s()]+/)
  fields = parse_fields
  return if type.nil? && fields.nil?
  type ||= 'query'
  raise_expected_not_found '{'.inspect if fields.nil?
  { type: type, args: args, fields: fields }
end
parse_field() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 185
def parse_field
  if chars[0, 3].join == '...'
    3.times { chars.shift }
    name = parse_name
    return ['...' + name, { fragment: name }]
  end
  name, alias_name = parse_name_alias
  return unless name
  consume_blank
  args = parse_args
  consume_blank
  fields = parse_fields
  [(alias_name || name), { field: name, params: args, attributes: fields }.compact]
end
parse_fields() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 200
def parse_fields
  return unless consume_text '{'
  consume_blank
  fields = {}
  loop do
    name, field = parse_field
    consume_blank
    consume_text ','
    consume_blank
    break unless name
    fields[name] = field
  end
  consume_text! '}'
  fields
end
parse_name() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 75
def parse_name
  name = ''
  name << chars.shift while chars.first && chars.first =~ /[a-zA-Z0-9_]/
  name unless name.empty?
end
parse_name_alias() click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 81
def parse_name_alias
  name = parse_name
  return unless name
  consume_blank
  if consume_text ':'
    consume_blank
    [parse_name, name]
  else
    name
  end
end
raise_expected_not_found(expected, found = nil) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 68
def raise_expected_not_found(expected, found = nil)
  raise(
    ParseError,
    "expected #{expected} but found #{found || chars.first.inspect} #{current_position_message}"
  )
end
unescape_string(s) click to toggle source
# File lib/ar_serializer/graphql/parser.rb, line 147
def unescape_string(s)
  JSON.parse %("#{s}")
rescue JSON::ParserError # for old json gem
  JSON.parse(%(["#{s}"])).first
end