All Files
(97.18%
covered at
27.54
hits/line)
12 files in total.
461 relevant lines.
448 lines covered and
13 lines missed
-
# frozen_string_literal: true
-
-
1
require_relative './gqli/dsl'
-
1
require_relative './gqli/client'
-
1
require_relative './gqli/introspection'
-
1
require_relative './gqli/version'
-
# frozen_string_literal: true
-
-
1
module GQLi
-
# Base class for GraphQL type wrappers
-
1
class Base
-
1
attr_reader :__name, :__depth, :__nodes
-
-
1
def initialize(name = nil, depth = 0, &block)
-
313
@__name = name
-
313
@__depth = depth
-
313
@__nodes = []
-
313
instance_eval(&block) unless block.nil?
-
end
-
-
# Inlines fragment nodes into current node
-
1
def ___(fragment)
-
10
@__nodes += __clone_nodes(fragment)
-
end
-
-
# Adds type match node
-
1
def __on(type_name, &block)
-
3
__node("... on #{type_name}", {}, &block)
-
end
-
-
# Adds children node into current node
-
1
def __node(name, params = {}, &block)
-
112
require_relative './node'
-
112
@__nodes << Node.new(name, params, __depth + 1, &block)
-
end
-
-
1
protected
-
-
1
def __clone_nodes(node_container)
-
182
require_relative './node'
-
182
__clone(node_container.__nodes).map do |n|
-
172
node = Node.new(n.__name, n.__params, __depth + 1)
-
172
node.instance_variable_set(
-
:@__nodes,
-
node.send(:__clone_nodes, n)
-
)
-
172
node
-
end
-
end
-
-
1
def __clone(obj)
-
182
Marshal.load(Marshal.dump(obj))
-
end
-
-
1
def __params_from_args(args)
-
107
args.empty? ? {} : args[0]
-
end
-
-
1
def method_missing(name, *args, &block)
-
107
__node(name.to_s, __params_from_args(args), &block)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'http'
-
1
require 'json'
-
1
require_relative './response'
-
1
require_relative './introspection'
-
1
require_relative './version'
-
-
1
module GQLi
-
# GraphQL HTTP Client
-
1
class Client
-
1
attr_reader :url, :params, :headers, :validate_query, :schema
-
-
1
def initialize(url, params: {}, headers: {}, validate_query: true)
-
13
@url = url
-
13
@params = params
-
13
@headers = headers
-
13
@validate_query = validate_query
-
-
13
@schema = Introspection.new(self) if validate_query
-
end
-
-
# Executes a query
-
# If validations are enabled, will perform validation check before request.
-
1
def execute(query)
-
3
if validate_query
-
2
validation = schema.validate(query)
-
2
fail validation_error_message(validation) unless validation.valid?
-
end
-
-
2
execute!(query)
-
end
-
-
# Executres a query
-
# Ignores validations
-
1
def execute!(query)
-
14
http_response = HTTP.headers(request_headers).post(@url, params: @params, json: { query: query.to_gql })
-
-
14
fail "Error: #{http_response.reason}\nBody: #{http_response.body}" if http_response.status >= 300
-
-
12
data = JSON.parse(http_response.to_s)['data']
-
-
12
Response.new(data, query)
-
end
-
-
# Validates a query against the schema
-
1
def valid?(query)
-
return true unless validate_query
-
-
schema.valid?(query)
-
end
-
-
1
protected
-
-
1
def validation_error_message(validation)
-
1
<<~ERROR
-
Validation Error: query is invalid - HTTP Request not sent.
-
-
Errors:
-
- #{validation.errors.join("\n - ")}
-
ERROR
-
end
-
-
1
def request_headers
-
{
-
accept: 'application/json',
-
user_agent: "gqli.rb/#{VERSION}; http.rb/#{HTTP::VERSION}"
-
14
}.merge(@headers)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative './query'
-
1
require_relative './fragment'
-
-
1
module GQLi
-
# GraphQL-like DSL methods
-
1
module DSL
-
# Creates a Query object
-
#
-
# Can be used at a class level
-
1
def self.query(name = nil, &block)
-
19
Query.new(name, &block)
-
end
-
-
# Creates a Fragment object
-
#
-
# Can be used at a class level
-
1
def self.fragment(name, on, &block)
-
3
Fragment.new(name, on, &block)
-
end
-
-
# Creates a Query object
-
#
-
# Can be used at an instance level
-
1
def query(name = nil, &block)
-
3
Query.new(name, &block)
-
end
-
-
# Creates a Fragment object
-
#
-
# Can be used at an instance level
-
1
def fragment(name, on, &block)
-
4
Fragment.new(name, on, &block)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative './base'
-
1
require_relative './node'
-
-
1
module GQLi
-
# Fragment wrapper
-
1
class Fragment < Base
-
1
attr_reader :__on_type
-
-
1
def initialize(name, on, &block)
-
7
super(name, 0, &block)
-
7
@__on_type = on
-
end
-
-
# Serializes to a GraphQL string
-
1
def to_gql
-
2
<<~GQL
-
fragment #{__name} on #{__on_type} {
-
#{__nodes.map(&:to_gql).join("\n")}
-
}
-
GQL
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative './dsl'
-
1
require_relative './validation'
-
-
1
module GQLi
-
# Introspection schema and validator
-
1
class Introspection
-
1
extend DSL
-
-
# Specific type kind introspection fragment
-
1
TypeRef = fragment('TypeRef', '__Type') {
-
1
kind
-
1
name
-
1
ofType {
-
1
kind
-
1
name
-
1
ofType {
-
1
kind
-
1
name
-
1
ofType {
-
1
kind
-
1
name
-
}
-
}
-
}
-
}
-
-
# Input value introspection fragment
-
1
InputValue = fragment('InputValue', '__InputValue') {
-
1
name
-
1
description
-
2
type { ___ TypeRef }
-
1
defaultValue
-
}
-
-
# Type introspection fragment
-
1
FullType = fragment('FullType', '__Type') {
-
1
kind
-
1
name
-
1
description
-
1
fields(includeDeprecated: true) {
-
1
name
-
1
description
-
2
args { ___ InputValue }
-
2
type { ___ TypeRef }
-
1
isDeprecated
-
1
deprecationReason
-
}
-
2
inputFields { ___ InputValue }
-
2
interfaces { ___ TypeRef }
-
1
enumValues(includeDeprecated: true) {
-
1
name
-
1
description
-
1
isDeprecated
-
1
deprecationReason
-
}
-
2
possibleTypes { ___ TypeRef }
-
}
-
-
# Query for fetching the complete schema
-
1
IntrospectionQuery = query {
-
1
__schema {
-
2
queryType { name }
-
2
mutationType { name }
-
2
subscriptionType { name }
-
2
types { ___ FullType }
-
1
directives {
-
1
name
-
1
description
-
2
args { ___ InputValue }
-
1
onOperation
-
1
onFragment
-
1
onField
-
}
-
}
-
}
-
-
1
attr_reader :schema, :query_type, :mutation_type, :subscription_type, :types
-
-
1
def initialize(client)
-
11
@schema = client.execute!(IntrospectionQuery).data.__schema
-
11
@query_type = schema.queryType
-
11
@mutation_type = schema.mutationType
-
11
@subscription_type = schema.subscriptionType
-
11
@types = schema.types
-
end
-
-
# Returns the evaluated validation for a query
-
1
def validate(query)
-
16
Validation.new(self, query)
-
end
-
-
# Returns if the query is valid
-
1
def valid?(query)
-
7
validate(query).valid?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative './base'
-
-
1
module GQLi
-
# Node wrapper
-
1
class Node < Base
-
1
attr_reader :__params
-
-
1
def initialize(name, params = {}, depth = 1, &block)
-
284
super(name, depth, &block)
-
284
@__params = params
-
end
-
-
# Serializes to a GraphQL string
-
1
def to_gql
-
1258
result = ' ' * __depth + __name
-
1258
result += '(' + __params_to_s(__params, true) + ')' unless __params.empty?
-
1258
unless __nodes.empty?
-
400
result += " {\n"
-
400
result += __nodes.map(&:to_gql).join("\n")
-
400
result += "\n#{' ' * __depth}}"
-
end
-
-
1258
result
-
end
-
-
1
private
-
-
1
def __params_to_s(params, initial = false)
-
57
case params
-
when ::Hash
-
28
result = params.map do |k, v|
-
28
"#{k}: #{__params_to_s(v)}"
-
end.join(', ')
-
-
28
return result if initial
-
1
"{#{result}}"
-
when ::Array
-
3
"[#{params.map { |p| __params_to_s(p) }.join(', ')}]"
-
when ::String
-
2
"\"#{params}\""
-
else
-
26
params.to_s
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative './base'
-
-
1
module GQLi
-
# Query node
-
1
class Query < Base
-
# Serializes to a GraphQL string
-
1
def to_gql
-
24
result = <<~GQL
-
query #{__name ? __name + ' ' : ''}{
-
#{__nodes.map(&:to_gql).join("\n")}
-
}
-
GQL
-
-
24
result.lstrip
-
end
-
-
# Delegates itself to the client to be executed
-
1
def __execute(client)
-
client.execute(self)
-
end
-
-
# Serializes to a GraphQL string
-
1
def to_s
-
to_gql
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'hashie/mash'
-
-
1
module GQLi
-
# Response object wrapper
-
1
class Response
-
1
attr_reader :data, :query
-
-
1
def initialize(data, query)
-
12
@data = Hashie::Mash.new(data)
-
12
@query = query
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module GQLi
-
# Validations
-
1
class Validation
-
1
attr_reader :schema, :query, :errors
-
-
1
def initialize(schema, query)
-
16
@schema = schema
-
16
@query = query
-
16
@errors = []
-
-
16
validate
-
end
-
-
# Returns wether the query is valid or not
-
1
def valid?
-
16
errors.empty?
-
end
-
-
1
protected
-
-
1
def validate
-
16
fail 'Not a Query object' unless query.is_a?(Query)
-
-
32
query_type = types.find { |t| t.name.casecmp('query').zero? }
-
16
query.__nodes.each do |node|
-
16
begin
-
16
validate_node(query_type, node)
-
rescue StandardError => e
-
9
errors << e
-
end
-
end
-
-
16
true
-
rescue StandardError => e
-
errors << e
-
end
-
-
1
private
-
-
1
def types
-
79
schema.types
-
end
-
-
1
def validate_node(parent_type, node)
-
52
return if parent_type.kind == 'SCALAR'
-
-
52
return valid_match_node?(parent_type, node) if node.__name.start_with?('... on')
-
-
295
node_type = parent_type.fetch('fields', []).find { |f| f.name == node.__name }
-
48
fail "Node type not found for '#{node.__name}'" if node_type.nil?
-
-
45
validate_params(node_type, node)
-
-
45
resolved_node_type = type_for(node_type)
-
45
fail "Node type not found for '#{node.__name}'" if resolved_node_type.nil?
-
-
45
validate_nesting_node(resolved_node_type, node)
-
-
77
node.__nodes.each { |n| validate_node(resolved_node_type, n) }
-
end
-
-
1
def valid_match_node?(parent_type, node)
-
14
return if parent_type.fetch('possibleTypes', []).find { |t| t.name == node.__name.gsub('... on ', '') }
-
2
fail "Match type '#{node.__name.gsub('... on ', '')}' invalid"
-
end
-
-
1
def validate_params(node_type, node)
-
45
node.__params.each do |param, value|
-
10
begin
-
48
arg = node_type.fetch('args', []).find { |a| a.name == param.to_s }
-
10
fail "Invalid argument '#{param}'" if arg.nil?
-
-
8
arg_type = type_for(arg)
-
8
fail "Argument type not found for '#{param}'" if arg_type.nil?
-
-
8
validate_value_for_type(arg_type, value, param)
-
rescue StandardError => e
-
4
errors << e
-
end
-
end
-
end
-
-
1
def validate_nesting_node(node_type, node)
-
45
fail "Invalid object for node '#{node.__name}'" unless valid_object_node?(node_type, node)
-
end
-
-
1
def valid_object_node?(node_type, node)
-
45
return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
-
41
true
-
end
-
-
1
def valid_array_node?(node_type, node)
-
return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
-
true
-
end
-
-
1
def value_type_error(is_type, should_be, for_arg)
-
2
fail "Value is '#{is_type}', but should be '#{should_be}' for '#{for_arg}'"
-
end
-
-
1
def validate_value_for_type(arg_type, value, for_arg)
-
14
case value
-
when ::String
-
8
unless arg_type.name == 'String' || arg_type.kind == 'ENUM' || arg_type.name == 'ID'
-
2
value_type_error('String, Enum or ID', arg_type.name, for_arg)
-
end
-
6
if arg_type.kind == 'ENUM' && !arg_type.enumValues.map(&:name).include?(value)
-
fail "Invalid value for Enum '#{arg_type.name}' for '#{for_arg}'"
-
end
-
when ::Integer
-
2
value_type_error('Integer', arg_type.name, for_arg) unless arg_type.name == 'Int'
-
when ::Float
-
value_type_error('Float', arg_type.name, for_arg) unless arg_type.name == 'Float'
-
when ::Hash
-
4
validate_hash_value(arg_type, value, for_arg)
-
when true, false
-
value_type_error('Boolean', arg_type.name, for_arg) unless arg_type.name == 'Boolean'
-
else
-
value_type_error(value.class.name, arg_type.name, for_arg)
-
end
-
end
-
-
1
def validate_hash_value(arg_type, value, for_arg)
-
4
value_type_error('Object', arg_type.name, for_arg) unless arg_type.kind == 'INPUT_OBJECT'
-
-
96
type = types.find { |f| f.name == arg_type.name }
-
4
fail "Type not found for '#{arg_type.name}'" if type.nil?
-
-
4
value.each do |k, v|
-
6
begin
-
104
input_field = type.fetch('inputFields', []).find { |f| f.name == k.to_s }
-
6
fail "Input field definition not found for '#{k}'" if input_field.nil?
-
-
6
input_field_type = type_for(input_field)
-
6
fail "Input field type not found for '#{k}'" if input_field_type.nil?
-
-
6
validate_value_for_type(input_field_type, v, k)
-
rescue StandardError => e
-
errors << e
-
end
-
end
-
end
-
-
1
def type_for(field_type)
-
59
type = case field_type.type.kind
-
when 'NON_NULL'
-
11
non_null_type(field_type.type.ofType)
-
when 'LIST'
-
2
field_type.type.ofType
-
when 'OBJECT', 'INTERFACE', 'INPUT_OBJECT'
-
21
field_type.type
-
when 'SCALAR'
-
25
field_type.type
-
end
-
-
542
types.find { |t| t.name == type.name }
-
end
-
-
1
def non_null_type(non_null)
-
11
case non_null.kind
-
when 'LIST'
-
11
non_null.ofType
-
else
-
non_null
-
end
-
end
-
end
-
end