From fc24020767a1ab0c69e146a4c045485efb09d911 Mon Sep 17 00:00:00 2001 From: Jordan Phillips Date: Tue, 4 Jun 2024 13:00:07 +1000 Subject: [PATCH] feat: add char scalar --- .../Core/src/Types.Scalars/CharType.cs | 146 ++++++++++++ .../Types.Scalars/ScalarResources.Designer.cs | 27 +++ .../src/Types.Scalars/ScalarResources.resx | 9 + .../Core/src/Types.Scalars/ThrowHelper.cs | 22 ++ .../src/Types.Scalars/WellKnownScalarTypes.cs | 1 + .../test/Types.Scalars.Tests/CharTypeTests.cs | 209 ++++++++++++++++++ ...arTypeTests.Schema_WithScalar_IsMatch.snap | 10 + 7 files changed, 424 insertions(+) create mode 100644 src/HotChocolate/Core/src/Types.Scalars/CharType.cs create mode 100644 src/HotChocolate/Core/test/Types.Scalars.Tests/CharTypeTests.cs create mode 100644 src/HotChocolate/Core/test/Types.Scalars.Tests/__snapshots__/CharTypeTests.Schema_WithScalar_IsMatch.snap 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