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
2 changes: 2 additions & 0 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class << self
autoload :AnalysisError, "graphql/analysis_error"
autoload :CoercionError, "graphql/coercion_error"
autoload :InvalidNameError, "graphql/invalid_name_error"
autoload :FloatDecodingError, "graphql/float_decoding_error"
autoload :FloatEncodingError, "graphql/float_encoding_error"
autoload :IntegerDecodingError, "graphql/integer_decoding_error"
autoload :IntegerEncodingError, "graphql/integer_encoding_error"
autoload :StringEncodingError, "graphql/string_encoding_error"
Expand Down
13 changes: 13 additions & 0 deletions lib/graphql/float_decoding_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module GraphQL
# This error is raised when `Types::Float` is given a non-finite input value.
class FloatDecodingError < GraphQL::RuntimeTypeError
# The value which couldn't be decoded
attr_reader :float_value

def initialize(value)
@float_value = value
super("Float is not finite: #{value.inspect}.")
end
end
end
28 changes: 28 additions & 0 deletions lib/graphql/float_encoding_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true
module GraphQL
# This error is raised when `Types::Float` is asked to return a non-finite value.
class FloatEncodingError < GraphQL::RuntimeTypeError
# The value which couldn't be encoded
attr_reader :float_value

# @return [GraphQL::Schema::Field] The field that returned a non-finite float
attr_reader :field

# @return [Array<String, Integer>] Where the field appeared in the GraphQL response
attr_reader :path

def initialize(value, context:)
@float_value = value
@field = context[:current_field]
@path = context[:current_path]
message = "Float is not finite: #{value.inspect}".dup
if @path
message << " @ #{@path.join(".")}"
end
if @field
message << " (#{@field.path})"
end
super("#{message}.")
end
end
end
4 changes: 2 additions & 2 deletions lib/graphql/language.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def self.serialize(value)
JSON.generate(value)
end
rescue JSON::GeneratorError
if Float::INFINITY == value
"Infinity"
if value.is_a?(Float) && !value.finite?
value.to_s
else
raise
end
Expand Down
17 changes: 16 additions & 1 deletion lib/graphql/query/variable_validation_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def to_h
# It is possible there are other extension items in this error, so handle
# a one level deep merge explicitly. However beyond that only show the
# latest value and problems.
super.merge({ "extensions" => { "value" => value, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
super.merge({ "extensions" => { "value" => value_for_extensions, "problems" => validation_result.problems }}) do |key, oldValue, newValue|
if oldValue.respond_to?(:merge)
oldValue.merge(newValue)
else
Expand All @@ -33,6 +33,21 @@ def to_h

private

def value_for_extensions(value = @value)
case value
when Array
value.map { |item| value_for_extensions(item) }
when Hash
value.each_with_object({}) do |(key, item), result|
result[key] = value_for_extensions(item)
end
when Float
value.finite? ? value : value.to_s
else
value
end
end

def problem_fields
@problem_fields ||= @validation_result
.problems
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1338,9 +1338,9 @@ def type_error(type_error, context)

context.errors << execution_error
execution_error
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::FloatEncodingError, GraphQL::IntegerEncodingError
raise type_error
when GraphQL::IntegerDecodingError
when GraphQL::FloatDecodingError, GraphQL::IntegerDecodingError
nil
end
end
Expand Down
22 changes: 18 additions & 4 deletions lib/graphql/types/float.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ module Types
class Float < GraphQL::Schema::Scalar
description "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)."

def self.coerce_input(value, _ctx)
value.is_a?(Numeric) ? value.to_f : nil
def self.coerce_input(value, ctx)
return if !value.is_a?(Numeric)

value = value.to_f
if value.finite?
value
else
err = GraphQL::FloatDecodingError.new(value)
ctx.schema.type_error(err, ctx)
end
end

def self.coerce_result(value, _ctx)
value.to_f
def self.coerce_result(value, ctx)
value = value.to_f
if value.finite?
value
else
err = GraphQL::FloatEncodingError.new(value, context: ctx)
ctx.schema.type_error(err, ctx)
end
end

default_scalar true
Expand Down
8 changes: 8 additions & 0 deletions spec/graphql/query/variable_validation_error_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,13 @@ def extensions
}
assert_equal error.to_h, as_hash
end

it 'makes non-finite values JSON-safe' do
error = subject.new(ast, type, { "values" => [Float::NAN, Float::INFINITY] }, validation_result)
value = error.to_h.dig("extensions", "value")

assert_equal({ "values" => ["NaN", "Infinity"] }, value)
JSON.generate(error.to_h)
end
end
end
66 changes: 66 additions & 0 deletions spec/graphql/types/float_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@

describe GraphQL::Types::Float do
let(:enum) { GraphQL::Language::Nodes::Enum.new(name: 'MILK') }
let(:schema) {
query_type = Class.new(GraphQL::Schema::Object) do
graphql_name "Query"

field :echo, Float do
argument :value, Float
end
field :non_finite, Float, resolve_static: true

def self.non_finite(_context)
::Float::NAN
end

def non_finite
self.class.non_finite(nil)
end
end

Class.new(GraphQL::Schema) do
query(query_type)
end
}

describe "coerce_input" do
it "accepts ints and floats" do
Expand All @@ -15,5 +37,49 @@
assert_nil GraphQL::Types::Float.coerce_isolated_input(true)
assert_nil GraphQL::Types::Float.coerce_isolated_input(enum)
end

it "rejects non-finite values" do
assert_nil GraphQL::Types::Float.coerce_isolated_input(Float::NAN)
assert_nil GraphQL::Types::Float.coerce_isolated_input(Float::INFINITY)
assert_nil GraphQL::Types::Float.coerce_isolated_input(-Float::INFINITY)
end

it "rejects non-finite literals and variables" do
literal_result = schema.execute("{ echo(value: 1e400) }")
assert_includes literal_result["errors"].first["message"], "has an invalid value"

variable_result = schema.execute(
"query($value: Float!) { echo(value: $value) }",
variables: { "value" => Float::NAN },
)
assert_includes variable_result["errors"].first["message"], "provided invalid value"
assert_equal "NaN", variable_result["errors"].first.dig("extensions", "value")
[literal_result, variable_result].each { |result| JSON.generate(result.to_h) }
end
end

describe "coerce_result" do
it "accepts finite values" do
assert_equal 1.0, GraphQL::Types::Float.coerce_isolated_result(1)
assert_equal 6.1, GraphQL::Types::Float.coerce_isolated_result(6.1)
end

it "raises on non-finite values" do
assert_raises(GraphQL::FloatEncodingError) do
GraphQL::Types::Float.coerce_isolated_result(Float::INFINITY)
end
assert_raises(GraphQL::FloatEncodingError) do
GraphQL::Types::Float.coerce_isolated_result(-Float::INFINITY)
end

err = assert_raises(GraphQL::FloatEncodingError) do
schema.execute("{ nonFinite }")
end
expected_message = exec_next_error_message(
"Query.nonFinite",
"Float is not finite: NaN#{if_exec_next("", " @ nonFinite (Query.nonFinite)")}.",
)
assert_equal expected_message, err.message
end
end
end
Loading