lib/elastic_graph/graphql/schema/arguments.rb



# Copyright 2024 Block, Inc.
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#
# frozen_string_literal: true

module ElasticGraph
  class GraphQL
    class Schema
      # A utility module for working with GraphQL schema arguments.
      module Arguments
        # A utility method to convert the given `args_hash` to its schema form.
        # The schema form is the casing of the arguments according to the GraphQL
        # schema definition.  For example, consider a case like:
        #
        # type Query {
        #   widgets(orderBy: [WidgetSort!]): [Widget!]!
        # }
        #
        # The GraphQL gem converts all arguments to ruby keyword args style (symbolized,
        # snake_case keys) before passing the args to us, but the `orderBy` argument in
        # the schema definition uses camelCase. ElasticGraph was designed to flexibly
        # support whatever casing the schema developer chooses to use but the GraphQL
        # gem's conversion to keyword args style gets in the way. ElasticGraph needs to
        # receive the arguments in the casing form defined in the schema so that, for example,
        # when it creates a datastore query, it correctly filters on fields according
        # to the casing of the fields in the index.
        #
        # This utility method converts an args hash back to its schema form (string keys,
        # with the casing from the schema) by using the arg definitions themselves to get
        # the arg names from the GraphQL schema.
        #
        # Example:
        #
        #   to_schema_form({ order_by: ["size"] }, widgets_field)
        #     # => { "orderBy" => ["size"] }
        #
        # The implementation here was taken from a code snippet provided by the maintainer of
        # the GraphQL gem: https://github.com/rmosolgo/graphql-ruby/issues/2869
        def self.to_schema_form(args_value, args_owner)
          # For custom scalar types (such as `_Any` for apollo federation), `args_owner` won't
          # response to `arguments`.
          return args_value unless args_owner.respond_to?(:arguments)

          __skip__ = case args_value
          when Hash, ::GraphQL::Schema::InputObject
            arg_defns = args_owner.arguments.values

            {}.tap do |accumulator|
              args_value.each do |key, value|
                # Note: we could build `arg_defns` into a hash keyed by `keyword`
                # outside of this loop, to give us an O(1) lookup here. However,
                # usually there are a small number of args (e.g 1 or 2, maybe up
                # to 6 in extreme cases) so it's probably likely to be ultimately
                # slower to build the hash, particularly when you account for the
                # extra memory allocation and GC for the hash.
                arg_defn = arg_defns.find do |a|
                  a.keyword == key
                end || raise(Errors::SchemaError, "Cannot find an argument definition for #{key.inspect} on `#{args_owner.name}`")

                next_owner = arg_defn.type.unwrap
                accumulator[arg_defn.name] = to_schema_form(value, next_owner)
              end
            end
          when Array
            args_value.map { |arg_value| to_schema_form(arg_value, args_owner) }
          else
            # :nocov: -- not sure how to cover this but we want this default branch.
            args_value
            # :nocov:
          end
        end
      end
    end
  end
end