diff --git a/src/HotChocolate/Core/src/Types.Scalars/CharType.cs b/src/HotChocolate/Core/src/Types.Scalars/CharType.cs
new file mode 100644
index 00000000000..a90bd4b90b2
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Scalars/CharType.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using HotChocolate.Language;
+
+namespace HotChocolate.Types;
+
+///
+/// The CharType scalar type represents a single character value.
+///
+public class CharType : ScalarType
+{
+ public CharType(
+ string name,
+ string? description = null,
+ BindingBehavior bind = BindingBehavior.Explicit)
+ : base(name, bind)
+ {
+ Description = description;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [ActivatorUtilitiesConstructor]
+ public CharType()
+ : this(
+ WellKnownScalarTypes.Char,
+ ScalarResources.CharType_Description)
+ {
+ }
+
+ ///
+ protected override bool IsInstanceOfType(StringValueNode valueSyntax)
+ {
+ return TryParseChar(valueSyntax.Value, out _);
+ }
+
+ ///
+ protected override char ParseLiteral(StringValueNode valueSyntax)
+ {
+ if (TryParseChar(valueSyntax.Value, out var character))
+ {
+ return character.Value;
+ }
+
+ throw ThrowHelper.CharType_ParseLiteral_IsInvalid(this);
+ }
+
+ ///
+ protected override StringValueNode ParseValue(char runtimeValue)
+ {
+ if (TryParseChar(runtimeValue, out var character))
+ {
+ return new StringValueNode(character.Value.ToString());
+ }
+
+ throw ThrowHelper.CharType_ParseValue_IsInvalid(this);
+ }
+
+ ///
+ public override IValueNode ParseResult(object? resultValue)
+ {
+ if (resultValue is null)
+ {
+ return NullValueNode.Default;
+ }
+
+ if (TryParseChar(resultValue, out var character))
+ {
+ return new StringValueNode(character.Value.ToString());
+ }
+
+ throw ThrowHelper.CharType_ParseValue_IsInvalid(this);
+ }
+
+ ///
+ public override bool TrySerialize(object? runtimeValue, out object? resultValue)
+ {
+ if (runtimeValue is null)
+ {
+ resultValue = null;
+ return true;
+ }
+
+ if (runtimeValue is char character)
+ {
+ resultValue = character;
+ return true;
+ }
+
+ if (TryParseChar(runtimeValue, out var c))
+ {
+ resultValue = c;
+ return true;
+ }
+
+ resultValue = null;
+ return false;
+ }
+
+ ///
+ public override bool TryDeserialize(object? resultValue, out object? runtimeValue)
+ {
+ if (resultValue is null)
+ {
+ runtimeValue = null;
+ return true;
+ }
+
+ if (resultValue is char character)
+ {
+ runtimeValue = character;
+ return true;
+ }
+
+ if (TryParseChar(resultValue, out var c))
+ {
+ runtimeValue = c;
+ return true;
+ }
+
+ runtimeValue = null;
+ return false;
+ }
+
+ ///
+ /// Attempts to convert the specified object to a char.
+ ///
+ /// The object to convert.
+ /// The char value equivalent to the value parameter if the conversion succeeded, or null if the conversion failed.
+ /// True if the conversion succeeded, otherwise false.
+ private static bool TryParseChar(object value, [NotNullWhen(true)] out char? character)
+ {
+ character = null;
+
+ try
+ {
+ character = Convert.ToChar(value);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.Designer.cs b/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.Designer.cs
index bb3473c2139..31fed605616 100644
--- a/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.Designer.cs
+++ b/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.Designer.cs
@@ -60,6 +60,33 @@ internal ScalarResources() {
}
}
+ ///
+ /// Looks up a localized string similar to The CharType scalar type represents a single character value..
+ ///
+ internal static string CharType_Description {
+ get {
+ return ResourceManager.GetString("CharType_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CharType cannot parse the provided literal. The provided value is not a valid single character..
+ ///
+ internal static string CharType_IsInvalid_ParseLiteral {
+ get {
+ return ResourceManager.GetString("CharType_IsInvalid_ParseLiteral", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CharType cannot parse the provided value. The provided value is not a valid single character..
+ ///
+ internal static string CharType_IsInvalid_ParseValue {
+ get {
+ return ResourceManager.GetString("CharType_IsInvalid_ParseValue", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The EmailAddress scalar type constitutes a valid email address, represented as a UTF-8 character sequence. The scalar follows the specification defined by the HTML Spec https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address..
///
diff --git a/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.resx b/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.resx
index f4c5943454d..18b7e98a293 100644
--- a/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.resx
+++ b/src/HotChocolate/Core/src/Types.Scalars/ScalarResources.resx
@@ -117,6 +117,15 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ The CharType scalar type represents a single character value.
+
+
+ CharType cannot parse the provided literal. The provided value is not a valid single character.
+
+
+ CharType cannot parse the provided value. The provided value is not a valid single character.
+
The EmailAddress scalar type constitutes a valid email address, represented as a UTF-8 character sequence. The scalar follows the specification defined by the HTML Spec https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address.
diff --git a/src/HotChocolate/Core/src/Types.Scalars/ThrowHelper.cs b/src/HotChocolate/Core/src/Types.Scalars/ThrowHelper.cs
index ec451e29b89..6b819c398e2 100644
--- a/src/HotChocolate/Core/src/Types.Scalars/ThrowHelper.cs
+++ b/src/HotChocolate/Core/src/Types.Scalars/ThrowHelper.cs
@@ -2,6 +2,28 @@ namespace HotChocolate.Types;
internal static class ThrowHelper
{
+ public static SerializationException CharType_ParseLiteral_IsInvalid(IType type)
+ {
+ return new SerializationException(
+ ErrorBuilder.New()
+ .SetMessage(ScalarResources.CharType_IsInvalid_ParseLiteral)
+ .SetCode(ErrorCodes.Scalars.InvalidSyntaxFormat)
+ .SetExtension("actualType", WellKnownScalarTypes.Char)
+ .Build(),
+ type);
+ }
+
+ public static SerializationException CharType_ParseValue_IsInvalid(IType type)
+ {
+ return new SerializationException(
+ ErrorBuilder.New()
+ .SetMessage(ScalarResources.CharType_IsInvalid_ParseValue)
+ .SetCode(ErrorCodes.Scalars.InvalidRuntimeType)
+ .SetExtension("actualType", WellKnownScalarTypes.Char)
+ .Build(),
+ type);
+ }
+
public static SerializationException EmailAddressType_ParseLiteral_IsInvalid(IType type)
{
return new SerializationException(
diff --git a/src/HotChocolate/Core/src/Types.Scalars/WellKnownScalarTypes.cs b/src/HotChocolate/Core/src/Types.Scalars/WellKnownScalarTypes.cs
index e2653c8cafd..1a3ed59b5d8 100644
--- a/src/HotChocolate/Core/src/Types.Scalars/WellKnownScalarTypes.cs
+++ b/src/HotChocolate/Core/src/Types.Scalars/WellKnownScalarTypes.cs
@@ -2,6 +2,7 @@ namespace HotChocolate.Types;
internal static class WellKnownScalarTypes
{
+ public const string Char = nameof(Char);
public const string EmailAddress = nameof(EmailAddress);
public const string HexColor = nameof(HexColor);
public const string Hsl = nameof(Hsl);
diff --git a/src/HotChocolate/Core/test/Types.Scalars.Tests/CharTypeTests.cs b/src/HotChocolate/Core/test/Types.Scalars.Tests/CharTypeTests.cs
new file mode 100644
index 00000000000..a6d8af47d45
--- /dev/null
+++ b/src/HotChocolate/Core/test/Types.Scalars.Tests/CharTypeTests.cs
@@ -0,0 +1,209 @@
+using System;
+using HotChocolate.Language;
+using Snapshooter.Xunit;
+
+namespace HotChocolate.Types;
+
+public class CharTypeTests : ScalarTypeTestBase
+{
+ [Fact]
+ public void Schema_WithScalar_IsMatch()
+ {
+ // arrange
+ var schema = BuildSchema();
+
+ // act
+ // assert
+ schema.ToString().MatchSnapshot();
+ }
+
+ [Theory]
+ [InlineData(typeof(EnumValueNode), TestEnum.Foo, false)]
+ [InlineData(typeof(IntValueNode), 1, false)]
+ [InlineData(typeof(BooleanValueNode), true, false)]
+ [InlineData(typeof(StringValueNode), "", false)]
+ [InlineData(typeof(StringValueNode), "a", true)]
+ [InlineData(typeof(StringValueNode), "7", true)]
+ [InlineData(typeof(StringValueNode), "\u263B", true)]
+ [InlineData(typeof(NullValueNode), null, true)]
+ public void IsInstanceOfType_GivenValueNode_MatchExpected(
+ Type type,
+ object value,
+ bool expected)
+ {
+ // arrange
+ var valueNode = CreateValueNode(type, value);
+
+ // act
+ // assert
+ ExpectIsInstanceOfTypeToMatch(valueNode, expected);
+ }
+
+ [Theory]
+ [InlineData(TestEnum.Foo, false)]
+ [InlineData(1, false)]
+ [InlineData(true, false)]
+ [InlineData("", false)]
+ [InlineData("a", false)]
+ [InlineData('a', true)]
+ [InlineData('7', true)]
+ [InlineData('\u263B', true)]
+ [InlineData(null, true)]
+ public void IsInstanceOfType_GivenObject_MatchExpected(object value, bool expected)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectIsInstanceOfTypeToMatch(value, expected);
+ }
+
+ [Theory]
+ [InlineData(typeof(StringValueNode), "a", 'a')]
+ [InlineData(typeof(StringValueNode), "7", '7')]
+ [InlineData(typeof(StringValueNode), "\u263B", '\u263b')]
+ [InlineData(typeof(NullValueNode), null, null)]
+ public void ParseLiteral_GivenValueNode_MatchExpected(
+ Type type,
+ object value,
+ object expected)
+ {
+ // arrange
+ var valueNode = CreateValueNode(type, value);
+
+ // act
+ // assert
+ ExpectParseLiteralToMatch(valueNode, expected);
+ }
+
+ [Theory]
+ [InlineData(typeof(EnumValueNode), TestEnum.Foo)]
+ [InlineData(typeof(FloatValueNode), 2.7d)]
+ [InlineData(typeof(IntValueNode), char.MinValue - 1)]
+ [InlineData(typeof(IntValueNode), char.MaxValue + 1)]
+ [InlineData(typeof(BooleanValueNode), true)]
+ [InlineData(typeof(StringValueNode), "")]
+ [InlineData(typeof(StringValueNode), "cool beans")]
+ public void ParseLiteral_GivenValueNode_ThrowSerializationException(Type type, object value)
+ {
+ // arrange
+ var valueNode = CreateValueNode(type, value);
+
+ // act
+ // assert
+ ExpectParseLiteralToThrowSerializationException(valueNode);
+ }
+
+ [Theory]
+ [InlineData(typeof(StringValueNode), 'a')]
+ [InlineData(typeof(StringValueNode), '7')]
+ [InlineData(typeof(StringValueNode), '\u263b')]
+ [InlineData(typeof(NullValueNode), null)]
+ public void ParseValue_GivenObject_MatchExpectedType(Type type, object value)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectParseValueToMatchType(value, type);
+ }
+
+ [Theory]
+ [InlineData(2.7d)]
+ [InlineData(true)]
+ [InlineData("")]
+ [InlineData("cool beans")]
+ public void ParseValue_GivenObject_ThrowSerializationException(object value)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectParseValueToThrowSerializationException(value);
+ }
+
+ [Theory]
+ [InlineData(65, 'A')]
+ [InlineData('a', 'a')]
+ [InlineData('7', '7')]
+ [InlineData('\u0007', '\u0007')]
+ [InlineData(null, null)]
+ public void Deserialize_GivenValue_MatchExpected(
+ object resultValue,
+ object runtimeValue)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectDeserializeToMatch(resultValue, runtimeValue);
+ }
+
+ [Theory]
+ [InlineData(2.7d)]
+ [InlineData(true)]
+ [InlineData("")]
+ [InlineData("cool beans")]
+ public void Deserialize_GivenValue_ThrowSerializationException(object value)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectDeserializeToThrowSerializationException(value);
+ }
+
+ [Theory]
+ [InlineData(65, 'A')]
+ [InlineData('a', 'a')]
+ [InlineData('7', '7')]
+ [InlineData('\u0007', '\u0007')]
+ [InlineData(null, null)]
+ public void Serialize_GivenObject_MatchExpectedType(
+ object runtimeValue,
+ object resultValue)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectSerializeToMatch(runtimeValue, resultValue);
+ }
+
+ [Theory]
+ [InlineData(2.7d)]
+ [InlineData(char.MinValue - 1)]
+ [InlineData(char.MaxValue + 1)]
+ [InlineData(true)]
+ [InlineData("")]
+ [InlineData("cool beans")]
+ public void Serialize_GivenObject_ThrowSerializationException(object value)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectSerializeToThrowSerializationException(value);
+ }
+
+ [Theory]
+ [InlineData(typeof(StringValueNode), 'a')]
+ [InlineData(typeof(StringValueNode), '7')]
+ [InlineData(typeof(StringValueNode), '\u263b')]
+ [InlineData(typeof(NullValueNode), null)]
+ public void ParseResult_GivenObject_MatchExpectedType(Type type, object value)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectParseResultToMatchType(value, type);
+ }
+
+ [Theory]
+ [InlineData(2.7d)]
+ [InlineData(char.MinValue - 1)]
+ [InlineData(char.MaxValue + 1)]
+ [InlineData(true)]
+ [InlineData("")]
+ [InlineData("cool beans")]
+ public void ParseResult_GivenObject_ThrowSerializationException(object value)
+ {
+ // arrange
+ // act
+ // assert
+ ExpectParseResultToThrowSerializationException(value);
+ }
+}
diff --git a/src/HotChocolate/Core/test/Types.Scalars.Tests/__snapshots__/CharTypeTests.Schema_WithScalar_IsMatch.snap b/src/HotChocolate/Core/test/Types.Scalars.Tests/__snapshots__/CharTypeTests.Schema_WithScalar_IsMatch.snap
new file mode 100644
index 00000000000..10b39a2ee13
--- /dev/null
+++ b/src/HotChocolate/Core/test/Types.Scalars.Tests/__snapshots__/CharTypeTests.Schema_WithScalar_IsMatch.snap
@@ -0,0 +1,10 @@
+schema {
+ query: Query
+}
+
+type Query {
+ scalar: Char
+}
+
+"The CharType scalar type represents a single character value."
+scalar Char