All Files
(94.02%
covered at
33.66
hits/line)
9 files in total.
251 relevant lines.
236 lines covered and
15 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)
-
255
@__name = name
-
255
@__depth = depth
-
255
@__nodes = []
-
255
instance_eval(&block) unless block.nil?
-
end
-
-
# Inlines fragment nodes into current node
-
1
def ___(fragment)
-
8
@__nodes += __clone_nodes(fragment)
-
end
-
-
# Adds type match node
-
1
def __on(type_name, &block)
-
2
require_relative './node'
-
2
@__nodes << Node.new("... on #{type_name}", [], __depth + 1, &block)
-
end
-
-
1
protected
-
-
1
def __clone_nodes(node_container)
-
178
require_relative './node'
-
178
__clone(node_container.__nodes).map do |n|
-
170
node = Node.new(n.__name, n.__params, __depth + 1)
-
170
node.instance_variable_set(
-
:@__nodes,
-
node.send(:__clone_nodes, n)
-
)
-
170
node
-
end
-
end
-
-
1
def __clone(obj)
-
178
Marshal.load(Marshal.dump(obj))
-
end
-
-
1
def __params_from_args(args)
-
72
args.empty? ? {} : args[0]
-
end
-
-
1
def method_missing(name, *args, &block)
-
72
require_relative './node'
-
72
@__nodes << Node.new(name.to_s, __params_from_args(args), __depth + 1, &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)
-
8
@url = url
-
8
@params = params
-
8
@headers = headers
-
8
@validate_query = validate_query
-
-
8
@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)
-
fail 'Validation Error: query is invalid - HTTP Request not sent' unless valid?(query)
-
-
execute!(query)
-
end
-
-
# Executres a query
-
# Ignores validations
-
1
def execute!(query)
-
8
http_response = HTTP.headers(request_headers).post(@url, params: @params, json: { query: query.to_gql })
-
-
8
fail "Error: #{http_response.reason}\nBody: #{http_response.body}" if http_response.status >= 300
-
-
8
data = JSON.parse(http_response.to_s)['data']
-
-
8
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 request_headers
-
{
-
accept: 'application/json',
-
user_agent: "gqli.rb/#{VERSION}; http.rb/#{HTTP::VERSION}"
-
8
}.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)
-
7
Query.new(name, &block)
-
end
-
-
# Creates a Fragment object
-
#
-
# Can be used at a class level
-
1
def self.fragment(name, on, &block)
-
Fragment.new(name, on, &block)
-
end
-
-
# Creates a Query object
-
#
-
# Can be used at an instance level
-
1
def query(name = nil, &block)
-
1
Query.new(name, &block)
-
end
-
-
# Creates a Fragment object
-
#
-
# Can be used at an instance level
-
1
def fragment(name, on, &block)
-
3
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)
-
3
super(name, 0, &block)
-
3
@__on_type = on
-
end
-
-
# Serializes to a GraphQL string
-
1
def to_gql
-
<<~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
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)
-
8
@schema = client.execute!(IntrospectionQuery).data.__schema
-
8
@query_type = schema.queryType
-
8
@mutation_type = schema.mutationType
-
8
@subscription_type = schema.subscriptionType
-
8
@types = schema.types
-
end
-
-
# Returns wether the query is valid or not
-
1
def valid?(query)
-
7
return false unless query.is_a?(Query)
-
-
14
query_type = types.find { |t| t.name.casecmp('query').zero? }
-
7
query.__nodes.each do |node|
-
7
return false unless valid_node?(query_type, node)
-
end
-
-
1
true
-
end
-
-
1
private
-
-
1
def valid_node?(parent_type, node)
-
20
return true if parent_type.kind == 'SCALAR'
-
-
20
return valid_match_node?(parent_type, node) if node.__name.start_with?('... on')
-
-
117
node_type = parent_type.fetch('fields', []).find { |f| f.name == node.__name }
-
18
return false if node_type.nil?
-
-
17
return false unless valid_params?(node_type, node)
-
-
15
resolved_node_type = type_for(node_type)
-
15
return false if resolved_node_type.nil?
-
-
15
return false unless valid_nesting_node?(resolved_node_type, node)
-
-
26
node.__nodes.all? { |n| valid_node?(resolved_node_type, n) }
-
end
-
-
1
def valid_match_node?(parent_type, node)
-
7
return true if parent_type.fetch('possibleTypes', []).find { |t| t.name == node.__name.gsub('... on ', '') }
-
1
false
-
end
-
-
1
def valid_params?(node_type, node)
-
17
node.__params.each do |param, value|
-
24
arg = node_type.fetch('args', []).find { |a| a.name == param.to_s }
-
5
return false if arg.nil?
-
-
4
arg_type = type_for(arg)
-
4
return false if arg_type.nil?
-
-
4
return false unless valid_value_for_type?(arg_type, value)
-
end
-
-
15
true
-
end
-
-
1
def valid_nesting_node?(node_type, node)
-
15
return false unless valid_object_node?(node_type, node)
-
13
return false unless valid_array_node?(node_type, node)
-
13
true
-
end
-
-
1
def valid_object_node?(node_type, node)
-
15
return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
-
13
true
-
end
-
-
1
def valid_array_node?(node_type, node)
-
13
return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
-
13
true
-
end
-
-
1
def valid_value_for_type?(arg_type, value)
-
7
case value
-
when ::String
-
4
return false unless arg_type.name == 'String' || arg_type.name == 'ID'
-
when ::Integer
-
1
return false unless arg_type.name == 'Int'
-
when ::Float
-
return false unless arg_type.name == 'Float'
-
when ::Hash
-
2
return valid_hash_value?(arg_type, value)
-
when true, false
-
return false unless arg_type.name == 'Boolean'
-
else
-
return false
-
end
-
-
4
true
-
end
-
-
1
def valid_hash_value?(arg_type, value)
-
2
return false unless arg_type.kind == 'INPUT_OBJECT'
-
-
48
type = types.find { |f| f.name == arg_type.name }
-
2
return false if type.nil?
-
-
2
value.each do |k, v|
-
52
input_field = type.fetch('inputFields', []).find { |f| f.name == k.to_s }
-
3
return false if input_field.nil?
-
-
3
input_field_type = type_for(input_field)
-
3
return false if input_field_type.nil?
-
-
3
return false unless valid_value_for_type?(input_field_type, v)
-
end
-
end
-
-
1
def type_for(field_type)
-
22
type = case field_type.type.kind
-
when 'NON_NULL'
-
3
non_null_type(field_type.type.ofType)
-
when 'LIST'
-
1
field_type.type.ofType
-
when 'OBJECT', 'INTERFACE', 'INPUT_OBJECT'
-
8
field_type.type
-
when 'SCALAR'
-
10
field_type.type
-
end
-
-
206
types.find { |t| t.name == type.name }
-
end
-
-
1
def non_null_type(non_null)
-
3
case non_null.kind
-
when 'LIST'
-
3
non_null.ofType
-
else
-
non_null
-
end
-
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)
-
244
super(name, depth, &block)
-
244
@__params = params
-
end
-
-
# Serializes to a GraphQL string
-
1
def to_gql
-
888
result = ' ' * __depth + __name
-
888
result += '(' + __params_to_s(__params, true) + ')' unless __params.empty?
-
888
unless __nodes.empty?
-
280
result += " {\n"
-
280
result += __nodes.map(&:to_gql).join("\n")
-
280
result += "\n#{' ' * __depth}}"
-
end
-
-
888
result
-
end
-
-
1
private
-
-
1
def __params_to_s(params, initial = false)
-
32
case params
-
when ::Hash
-
16
result = params.map do |k, v|
-
16
"#{k}: #{__params_to_s(v)}"
-
end.join(', ')
-
-
16
return result if initial
-
"{#{result}}"
-
when ::Array
-
"[#{params.map { |p| __params_to_s(p) }.join(', ')}]"
-
when ::String
-
"\"#{params}\""
-
else
-
16
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
-
8
result = <<~GQL
-
query #{__name ? __name + ' ' : ''}{
-
#{__nodes.map(&:to_gql).join("\n")}
-
}
-
GQL
-
-
8
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)
-
8
@data = Hashie::Mash.new(data)
-
8
@query = query
-
end
-
end
-
end