Skip to content

Commit

Permalink
Fixed List Input Coercion (#250)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Sep 2, 2018
1 parent a539208 commit 6a2b153
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,25 @@ public void ConditionalInlineFragment()
Assert.Equal(Snapshot.Current(), Snapshot.New(result));
}

[Fact]
public void EnumValueIsCoercedToListValue()
{
// arrange
Schema schema = CreateSchema();
string query = @"
{
heroes(episodes: EMPIRE) {
name
}
}";

// act
IExecutionResult result = schema.Execute(query);

// assert
Assert.Equal(Snapshot.Current(), Snapshot.New(result));
}

private static Schema CreateSchema()
{
CharacterRepository repository = new CharacterRepository();
Expand Down
197 changes: 197 additions & 0 deletions src/Core.Tests/Types/InputCoercionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using System;
using HotChocolate.Language;
using Xunit;

namespace HotChocolate.Types
{
public class InputCoercionTests
{
/// <summary>
/// Converts according to input coercion rules.
/// </summary>
[Fact]
public void ConvertAccordingToInputCoercionRules()
{
InputIsCoercedCorrectly<BooleanType, BooleanValueNode, bool>(
new BooleanValueNode(true), true);
InputIsCoercedCorrectly<BooleanType, BooleanValueNode, bool>(
new BooleanValueNode(false), false);
InputIsCoercedCorrectly<IntType, IntValueNode, int>(
new IntValueNode("123"), 123);
InputIsCoercedCorrectly<FloatType, IntValueNode, double>(
new IntValueNode("123"), 123d);
InputIsCoercedCorrectly<FloatType, FloatValueNode, double>(
new FloatValueNode("123.456"), 123.456d);
InputIsCoercedCorrectly<StringType, StringValueNode, string>(
new StringValueNode("abc123"), "abc123");
InputIsCoercedCorrectly<IdType, StringValueNode, string>(
new StringValueNode("123456"), "123456");
}

/// <summary>
/// Does not convert when input coercion rules reject a value.
/// </summary>
[Fact]
public void ConvertAccordingToInputCoercionRules2()
{
InputCannotBeCoercedCorrectly<BooleanType, IntValueNode>(
new IntValueNode("123"));
InputCannotBeCoercedCorrectly<IntType, FloatValueNode>(
new FloatValueNode("123.123"));
InputCannotBeCoercedCorrectly<IntType, BooleanValueNode>(
new BooleanValueNode(true));
InputCannotBeCoercedCorrectly<IntType, StringValueNode>(
new StringValueNode("123.123"));
InputCannotBeCoercedCorrectly<FloatType, StringValueNode>(
new StringValueNode("123"));
InputCannotBeCoercedCorrectly<StringType, FloatValueNode>(
new FloatValueNode("123.456"));
InputCannotBeCoercedCorrectly<StringType, BooleanValueNode>(
new BooleanValueNode(false));
InputIsCoercedCorrectly<IdType, StringValueNode, string>(
new StringValueNode("123456"), "123456");
}

[Fact]
public void InputListIsInstanceOf()
{
InputListIsInstanceOfInternal<BooleanType>(
new ListValueNode(new BooleanValueNode(true)));
InputListIsInstanceOfInternal<BooleanType>(
new BooleanValueNode(true));

InputListIsNotInstanceOfInternal<BooleanType>(
new ListValueNode(new IValueNode[] {
new BooleanValueNode(true),
new StringValueNode("123") }));
InputListIsNotInstanceOfInternal<BooleanType>(
new StringValueNode("123"));
}

[Fact]
public void ListCanBeCoercedFromListValue()
{
// arrange
var type = new ListType(new BooleanType());
var list = new ListValueNode(
new[] {
new BooleanValueNode(true),
new BooleanValueNode(false)});

// act
object coercedValue = type.ParseLiteral(list);

// assert
Assert.IsType<bool[]>(coercedValue);
Assert.Collection((bool[])coercedValue,
t => Assert.True(t),
t => Assert.False(t));
}

[Fact]
public void ListCanBeCoercedFromListElementValue()
{
// arrange
var type = new ListType(new BooleanType());
var element = new BooleanValueNode(true);

// act
object coercedValue = type.ParseLiteral(element);

// assert
Assert.IsType<bool[]>(coercedValue);
Assert.Collection((bool[])coercedValue,
t => Assert.True(t));
}

[Fact]
public void ListCannotBeCoercedFromMixedList()
{
// arrange
var type = new ListType(new BooleanType());
var list = new ListValueNode(
new IValueNode[] {
new BooleanValueNode(true),
new StringValueNode("foo")});

// act
Action action = () => type.ParseLiteral(list);

// assert
Assert.Throws<ArgumentException>(action);
}

[Fact]
public void ListCannotBeCoercedIfElementTypeDoesNotMatch()
{
// arrange
var type = new ListType(new BooleanType());
var element = new StringValueNode("foo");

// act
Action action = () => type.ParseLiteral(element);

// assert
Assert.Throws<ArgumentException>(action);
}

private void InputIsCoercedCorrectly<TType, TLiteral, TExpected>(
TLiteral literal, TExpected expectedValue)
where TType : ScalarType, new()
where TLiteral : IValueNode
{
// arrange
var type = new TType();

// act
object coercedValue = type.ParseLiteral(literal);

// assert
Assert.IsType<TExpected>(coercedValue);
Assert.Equal(expectedValue, coercedValue);
}

private void InputCannotBeCoercedCorrectly<TType, TLiteral>(
TLiteral literal)
where TType : ScalarType, new()
where TLiteral : IValueNode
{
// arrange
var type = new TType();

// act
Action action = () => type.ParseLiteral(literal);

// assert
Assert.Throws<ArgumentException>(action);
}

private void InputListIsInstanceOfInternal<TElement>(
IValueNode literal)
where TElement : ScalarType, new()
{
// arrange
var type = new ListType(new TElement());

// act
bool isInstanceOfType = type.IsInstanceOfType(literal);

// assert
Assert.True(isInstanceOfType);
}

private void InputListIsNotInstanceOfInternal<TElement>(
IValueNode literal)
where TElement : ScalarType, new()
{
// arrange
var type = new ListType(new TElement());

// act
bool isInstanceOfType = type.IsInstanceOfType(literal);

// assert
Assert.False(isInstanceOfType);
}
}
}
2 changes: 1 addition & 1 deletion src/Core.Tests/Types/NonNullTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void EnsureInstanceOfIsDelegatedToInnerType()

NonNullType type = new NonNullType(innerType);
bool shouldBeFalse = type.IsInstanceOfType(
new StringValueNode("foo"));
new IntValueNode("123"));
bool shouldBeTrue = type.IsInstanceOfType(
new ListValueNode(new[] { new StringValueNode("foo") }));

Expand Down
10 changes: 10 additions & 0 deletions src/Core.Tests/__snapshots__/EnumValueIsCoercedToListValue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Data": {
"heroes": [
{
"name": "Luke Skywalker"
}
]
},
"Errors": null
}
19 changes: 19 additions & 0 deletions src/Language/AST/ListValueNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ public sealed class ListValueNode
{
private int? _hash;

public ListValueNode(IValueNode item)
: this(null, item)
{
}

public ListValueNode(Location location, IValueNode item)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}

var items = new List<IValueNode>(1);
items.Add(item);

Location = location;
Items = items.AsReadOnly();
}

public ListValueNode(
IReadOnlyList<IValueNode> items)
: this(null, items)
Expand Down
66 changes: 45 additions & 21 deletions src/Types/Types/ListType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,31 +54,38 @@ public bool IsInstanceOfType(IValueNode literal)

if (_isInputType)
{
if (literal is NullValueNode)
{
return true;
}
return IsInstanceOfTypeInternal(literal);
}

throw new InvalidOperationException(
"The specified type is not an input type.");
}

private bool IsInstanceOfTypeInternal(IValueNode literal)
{
if (literal is NullValueNode)
{
return true;
}

if (literal is ListValueNode listValueLiteral)
if (_inputType.IsInstanceOfType(literal))
{
return true;
}

if (literal is ListValueNode listValueLiteral)
{
foreach (IValueNode element in listValueLiteral.Items)
{
if (listValueLiteral.Items.Any())
if (!_inputType.IsInstanceOfType(element))
{
IValueNode value = listValueLiteral.Items.First();
if (!_inputType.IsInstanceOfType(value))
{
return !ElementType.IsNonNullType()
&& value is NullValueNode;
}

return true;
return false;
}
return true;
}
return false;
}

throw new InvalidOperationException(
"The specified type is not an input type.");
return true;
}
return false;
}

public object ParseLiteral(IValueNode literal)
Expand All @@ -88,18 +95,35 @@ public object ParseLiteral(IValueNode literal)
throw new ArgumentNullException(nameof(literal));
}

if (_isInputType)
{
return ParseLiteralInternal(literal);
}

throw new InvalidOperationException(
"The specified type is not an input type.");
}

private object ParseLiteralInternal(IValueNode literal)
{
if (literal is NullValueNode)
{
return null;
}

if (_inputType.IsInstanceOfType(literal))
{
return CreateArray(new ListValueNode(literal));
}


if (_isInputType && literal is ListValueNode listValueLiteral)
{
return CreateArray(listValueLiteral);
}

throw new InvalidOperationException(
"The specified type is not an input type.");
throw new ArgumentException(
"The specified literal cannot be handled by this list type.");
}

private object CreateArray(ListValueNode listValueLiteral)
Expand Down

0 comments on commit 6a2b153

Please sign in to comment.