Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,17 @@ jobs:
gem install bundler -v 2.4.22
bundle install --jobs 4 --retry 3
bundle exec rake test

typecheck:
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: Gemfile
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
bundler-cache: true
- name: Run typecheck
run: bundle exec rake typecheck
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
source 'https://rubygems.org'
gemspec

group :development do
gem 'sorbet', require: false
gem 'tapioca', require: false
end

gem 'pry'
gem 'pry-byebug'
gem 'warning'
Expand Down
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ namespace :benchmark do
end
end

desc "Run Sorbet typecheck"
task :typecheck do
sh "bundle exec srb tc"
end

desc "Run benchmarks"
task benchmark: "benchmark:planner"

Expand Down
16 changes: 16 additions & 0 deletions bin/tapioca
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'tapioca' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

require "rubygems"
require "bundler/setup"

load Gem.bin_path("tapioca", "tapioca")
57 changes: 29 additions & 28 deletions lib/graphql/stitching.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
# frozen_string_literal: true
# typed: true

require "graphql"

module GraphQL
module Stitching
# scope name of query operations.
QUERY_OP = "query"

# scope name of mutation operations.
MUTATION_OP = "mutation"

# scope name of subscription operations.
SUBSCRIPTION_OP = "subscription"

# introspection typename field.
TYPENAME = "__typename"

# @api private
EMPTY_OBJECT = {}.freeze

# @api private
EMPTY_ARRAY = [].freeze
QUERY_OP = "query".freeze #: String

MUTATION_OP = "mutation".freeze #: String

SUBSCRIPTION_OP = "subscription".freeze #: String

TYPENAME = "__typename".freeze #: String

EMPTY_OBJECT = {}.freeze #: Hash[untyped, untyped]

EMPTY_ARRAY = [].freeze #: Array[untyped]

class StitchingError < StandardError; end
class CompositionError < StitchingError; end
class ValidationError < CompositionError; end
class DocumentError < StandardError
#: (String element) -> void
def initialize(element)
super("Invalid #{element} encountered in document")
end
end

MIN_VISIBILITY_VERSION = "2.5.3".freeze #: String

class << self
# Proc used to compute digests; uses SHA2 by default.
# @returns [Proc] proc used to compute digests.
# @rbs!
# @digest: ^(String) -> String
# @stitch_directive: String
# @visibility_directive: String
# @supports_visibility: bool

#: ?{ (String) -> String } -> ^(String) -> String
def digest(&block)
if block_given?
@digest = block
Expand All @@ -42,25 +45,23 @@ def digest(&block)
end
end

# Name of the directive used to mark type resolvers.
# @returns [String] name of the type resolver directive.
#: -> String
def stitch_directive
@stitch_directive ||= "stitch"
@stitch_directive ||= "stitch".freeze
end

#: String
attr_writer :stitch_directive

# Name of the directive used to denote member visibilities.
# @returns [String] name of the visibility directive.
#: -> String
def visibility_directive
@visibility_directive ||= "visibility"
@visibility_directive ||= "visibility".freeze
end

#: String
attr_writer :visibility_directive

MIN_VISIBILITY_VERSION = "2.5.3"

# @returns Boolean true if GraphQL::Schema::Visibility is fully supported
#: -> bool
def supports_visibility?
return @supports_visibility if defined?(@supports_visibility)

Expand Down
41 changes: 27 additions & 14 deletions lib/graphql/stitching/client.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
# frozen_string_literal: true
# typed: true

require "json"

module GraphQL
module Stitching
# Client is an out-of-the-box helper that assembles all
# stitching components into a workflow that executes requests.
class Client
class << self
#: (String | singleton(GraphQL::Schema) schema, executables: Hash[Location | Symbol, Executable]) -> Client
def from_definition(schema, executables:)
new(supergraph: Supergraph.from_definition(schema, executables: executables))
end
end

# @return [Supergraph] composed supergraph that services incoming requests.
#: Supergraph
attr_reader :supergraph

# Builds a new client instance. Either `supergraph` or `locations` configuration is required.
# @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
# @param locations [Hash<Symbol, Hash<Symbol, untyped>>] optional, composer configurations for each graph location.
# @param composer_options [Hash] optional, composer options for configuring composition.
#: (?locations: untyped, ?supergraph: Supergraph?, ?composer_options: Hash[Symbol, untyped]) -> void
def initialize(locations: nil, supergraph: nil, composer_options: {})
@supergraph = if locations && supergraph
raise ArgumentError, "Cannot provide both locations and a supergraph."
Expand All @@ -34,15 +31,26 @@ def initialize(locations: nil, supergraph: nil, composer_options: {})
composer.perform(locations)
end

@on_cache_read = nil
@on_cache_write = nil
@on_error = nil
@on_cache_read = nil #: CacheReadHandler?
@on_cache_write = nil #: CacheWriteHandler?
@on_error = nil #: ErrorHandler?
end

#: (
#| ?String | DocumentNode | nil raw_query,
#| ?query: String | DocumentNode | nil,
#| ?variables: Variables?,
#| ?operation_name: String?,
#| ?context: untyped,
#| ?validate: bool
#| ) -> untyped
def execute(raw_query = nil, query: nil, variables: nil, operation_name: nil, context: nil, validate: true)
source = raw_query || query
raise ArgumentError, "A query string or document is required." unless source

request = Request.new(
@supergraph,
raw_query || query, # << for parity with GraphQL Ruby Schema.execute
source,
operation_name: operation_name,
variables: variables,
context: context,
Expand All @@ -62,23 +70,27 @@ def execute(raw_query = nil, query: nil, variables: nil, operation_name: nil, co
error_result(request, [{ "message" => custom_message || "An unexpected error occured." }])
end

#: ?{ (Request) -> String? } -> CacheReadHandler
def on_cache_read(&block)
raise ArgumentError, "A cache read block is required." unless block_given?
raise ArgumentError, "A cache read block is required." unless block
@on_cache_read = block
end

#: ?{ (Request, String) -> void } -> CacheWriteHandler
def on_cache_write(&block)
raise ArgumentError, "A cache write block is required." unless block_given?
raise ArgumentError, "A cache write block is required." unless block
@on_cache_write = block
end

#: ?{ (Request?, StandardError) -> String? } -> ErrorHandler
def on_error(&block)
raise ArgumentError, "An error handler block is required." unless block_given?
raise ArgumentError, "An error handler block is required." unless block
@on_error = block
end

private

#: (Request request) -> Plan
def load_plan(request)
if @on_cache_read && plan_json = @on_cache_read.call(request)
plan = Plan.from_json(JSON.parse(plan_json))
Expand All @@ -98,6 +110,7 @@ def load_plan(request)
plan
end

#: (Request? request, Array[PublicErrorObject | PublicError] errors) -> GraphQL::Query::Result
def error_result(request, errors)
public_errors = errors.map! do |e|
e.is_a?(Hash) ? e : e.to_h
Expand Down
Loading
Loading