diff --git a/Directory.Build.props b/Directory.Build.props index 81f37f4..6809d8a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ Domain Primitives ALTA Software llc. Copyright © 2024 ALTA Software llc. - 8.0.0 + 8.0.1 diff --git a/Examples/AltaSoft.DomainPrimitives.Demo/Customer.cs b/Examples/AltaSoft.DomainPrimitives.Demo/Customer.cs index fffbd93..03c41a2 100644 --- a/Examples/AltaSoft.DomainPrimitives.Demo/Customer.cs +++ b/Examples/AltaSoft.DomainPrimitives.Demo/Customer.cs @@ -7,7 +7,8 @@ public sealed record Customer( [Required] CustomerId A_CustomerId, [Required] Guid A_CustomerIdDotNet, [Required] BirthDate B_BirthDate, [Required] DateOnly B_BirthDateDotNet, - [Required] CustomerName C_CustomerName, [Required] string C_CustomerNameDotNet, + [Required] CustomerName C_CustomerName, + [Required][property: RegularExpression("\\Axxx")] string C_CustomerNameDotNet, [Required] PositiveAmount D_Amount, [Required] decimal D_AmountDotnet) { public CustomerAddress? CustomerAddress { get; set; } //ignore diff --git a/src/AltaSoft.DomainPrimitives.Generator/Executor.cs b/src/AltaSoft.DomainPrimitives.Generator/Executor.cs index fd3ab35..5cb8b79 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Executor.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Executor.cs @@ -9,6 +9,7 @@ using AltaSoft.DomainPrimitives.Generator.Helpers; using AltaSoft.DomainPrimitives.Generator.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace AltaSoft.DomainPrimitives.Generator; @@ -389,6 +390,11 @@ private static void Process(GeneratorData data, string ctorCode, DomainPrimitive usings.Add("System.Globalization"); } + if (data.ValidatePattern) + { + usings.Add("System.Text.RegularExpressions"); + } + var needsMathOperators = data.GenerateAdditionOperators || data.GenerateDivisionOperators || data.GenerateMultiplyOperators || data.GenerateSubtractionOperators || data.GenerateModulusOperator; @@ -780,7 +786,11 @@ private static bool ProcessConstructor(GeneratorData data, SourceCodeBuilder bui .OpenBracket(); if (data.UnderlyingType == DomainPrimitiveUnderlyingType.String) + { AddStringLengthAttributeValidation(type, data, builder); + AddPatternAttribute(type, data, builder); + + } builder.AppendLine("ValidateOrThrow(value);"); builder.CloseBracket() @@ -836,4 +846,33 @@ private static void AddStringLengthAttributeValidation(ISymbol domainPrimitiveTy .AppendLine($"\tthrow InvalidDomainValueException.StringRangeException(typeof({data.ClassName}), value, {minValue.ToString(CultureInfo.InvariantCulture)}, {maxValue.ToString(CultureInfo.InvariantCulture)});") .NewLine(); } + + /// + /// Adds pattern validation to the constructor if the Domain Primitive type is decorated with the PatternAttribute. + /// + private static void AddPatternAttribute(ISymbol domainPrimitiveType, GeneratorData data, SourceCodeBuilder sb) + { + var attr = domainPrimitiveType.GetAttributes() + .FirstOrDefault(x => string.Equals(x.AttributeClass?.ToDisplayString(), Constants.PatternAttributeFullName, StringComparison.Ordinal)); + + if (attr is null) + return; + + var pattern = (string)attr.ConstructorArguments[0].Value!; + var validate = (bool)attr.ConstructorArguments[1].Value!; + + if (string.IsNullOrEmpty(pattern)) + return; + + data.Pattern = pattern; + data.ValidatePattern = validate; + var quotedPattern = SymbolDisplay.FormatLiteral(data.Pattern, quote: true); + + if (validate) + { + sb.AppendLine($"if (!Regex.IsMatch(value, {quotedPattern}, RegexOptions.Compiled))") + .AppendLine($"\tthrow InvalidDomainValueException.InvalidPatternException(typeof({data.ClassName}), value, {quotedPattern});") + .NewLine(); + } + } } diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/Constants.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/Constants.cs index 3e82212..6de0be4 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/Constants.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/Constants.cs @@ -7,5 +7,6 @@ internal static class Constants internal const string SupportedOperationsAttribute = "SupportedOperationsAttribute"; internal const string SupportedOperationsAttributeFullName = "AltaSoft.DomainPrimitives.SupportedOperationsAttribute"; internal const string StringLengthAttributeFullName = "AltaSoft.DomainPrimitives.StringLengthAttribute"; + internal const string PatternAttributeFullName = "AltaSoft.DomainPrimitives.PatternAttribute"; } } diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs index 7b4426f..238458a 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.Linq; using AltaSoft.DomainPrimitives.Generator.Extensions; using AltaSoft.DomainPrimitives.Generator.Models; @@ -112,6 +113,18 @@ void AddMapping() } } + if (data.StringLengthAttributeValidation is { } stringLengthAttribute) + { + var (min, max) = stringLengthAttribute; + builder.Append("MinLength = ").Append(min.ToString(CultureInfo.InvariantCulture)).AppendLine(","); + builder.Append("MaxLength = ").Append(max.ToString(CultureInfo.InvariantCulture)).AppendLine(","); + } + + if (data.Pattern is not null) + { + builder.Append("Pattern = ").Append(QuoteAndEscape(data.Pattern)).AppendLine(","); + } + builder.Length -= SourceCodeBuilder.s_newLineLength + 1; builder.NewLine(); builder.AppendLine("}"); @@ -424,6 +437,7 @@ internal static void GenerateMandatoryMethods(GeneratorData data, SourceCodeBuil } AddStringLengthValidation(data, builder); + AddPatternValidation(data, builder); builder.AppendLine("var validationResult = Validate(value);") .AppendLine("if (!validationResult.IsValid)") @@ -477,6 +491,24 @@ static void AddStringLengthValidation(GeneratorData data, SourceCodeBuilder sb) .CloseBracket() .NewLine(); } + + static void AddPatternValidation(GeneratorData data, SourceCodeBuilder sb) + { + if (data.Pattern is null) + return; + + if (!data.ValidatePattern) + return; + + var quoted = QuoteAndEscape(data.Pattern); + sb.AppendLine($"if (!Regex.IsMatch(value, {quoted}, RegexOptions.Compiled))") + .OpenBracket() + .AppendLine("result = null;") + .AppendLine($"errorMessage = \"String does not match the required pattern: \" + {quoted};") + .AppendLine("return false;") + .CloseBracket() + .NewLine(); + } } /// diff --git a/src/AltaSoft.DomainPrimitives.Generator/Models/GeneratorData.cs b/src/AltaSoft.DomainPrimitives.Generator/Models/GeneratorData.cs index a517be0..95ada09 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Models/GeneratorData.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Models/GeneratorData.cs @@ -127,4 +127,15 @@ internal sealed class GeneratorData /// Indicates whether the `Transform` method should be invoked before validation and instantiation. /// public bool UseTransformMethod { get; set; } + + /// + /// Gets or sets pattern for OpenAPI schema generation, or validation. + /// + public string? Pattern { get; set; } + + /// + /// Gets or sets a value indicating whether to validate the regex pattern at runtime when the `PatternAttribute` is applied to a Domain Primitive type. If set to `true`, the generated code will include logic to validate the pattern during instantiation and throw an exception if the value does not match the specified regex pattern. + /// If set to `false`, the pattern will only be used for OpenAPI schema generation and will not be validated at runtime. + /// + public bool ValidatePattern { get; set; } } diff --git a/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs b/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs index e8238da..02ff306 100644 --- a/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs +++ b/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs @@ -68,6 +68,19 @@ public static InvalidDomainValueException LimitExceededException(Type type, int return new InvalidDomainValueException($"The value has exceeded a {underlyingTypeName} limit", type, value); } + /// + /// Creates an for string pattern mismatch errors. + /// + /// The of the domain primitive. + /// The string value that failed to match the pattern. + /// The expected regex pattern. + /// An describing the pattern mismatch. + [EditorBrowsable(EditorBrowsableState.Never)] + public static InvalidDomainValueException InvalidPatternException(Type type, string value, string pattern) + { + return new InvalidDomainValueException($"String value does not match the required pattern '{pattern}'", type, value); + } + /// /// Generates the error message for the including the underlying value. /// diff --git a/src/AltaSoft.DomainPrimitives/PatternAttribute.cs b/src/AltaSoft.DomainPrimitives/PatternAttribute.cs new file mode 100644 index 0000000..de6e351 --- /dev/null +++ b/src/AltaSoft.DomainPrimitives/PatternAttribute.cs @@ -0,0 +1,39 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AltaSoft.DomainPrimitives; + +/// +/// Specifies a regex pattern that is always emitted to the OpenAPI schema and can optionally be used for runtime validation. +/// By default, the pattern is not validated at runtime; validation occurs only when explicitly requested via . +/// +[AttributeUsage(AttributeTargets.Class)] +public class PatternAttribute : Attribute +{ + /// + /// Gets the regex pattern used in the generated OpenAPI schema and, when enabled, for runtime validation. + /// + public string Pattern { get; } + + /// + /// Gets a value indicating whether the should also be enforced via runtime validation. + /// + public bool Validate { get; } + + /// + /// Initializes a new instance of . + /// + /// + /// The regex pattern that will always be included in the OpenAPI schema and may also be used for runtime validation. + /// + /// + /// A value indicating whether runtime validation should be performed using . Defaults to + /// to avoid incurring runtime validation overhead unless explicitly requested. + /// + + public PatternAttribute([StringSyntax(StringSyntaxAttribute.Regex)] string pattern, bool validate = false) + { + Pattern = pattern; + Validate = validate; + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs index dbba32a..4f2fc5d 100644 --- a/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs @@ -5,6 +5,76 @@ namespace AltaSoft.DomainPrimitives.Generator.Tests; public class DomainPrimitiveGeneratorTest { + [Fact] + public Task StringValue_WithTransStringLengthAndPattern() + { + const string source = """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using AltaSoft.DomainPrimitives; + + namespace AltaSoft.DomainPrimitives; + + /// + /// A string domain primitive with both length and pattern validation attributes, as well as a custom validation method. + /// + [StringLength(1, 100)] + [Pattern(@"[A-Z]{100}")] + internal partial class StringWithLengthAndPattern : IDomainValue + { + /// + public static PrimitiveValidationResult Validate(string value) + { + if (value == "Test") + return "Invalid Value"; + + return PrimitiveValidationResult.Ok; + } + } + + """; + + return TestHelper.Verify(source, (_, x, _) => Assert.Equal(4, x.Count)); + } + + [Fact] + public Task StringValue_WithTransStringLengthAndPatternWithValidation() + { + const string source = """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using AltaSoft.DomainPrimitives; + + namespace AltaSoft.DomainPrimitives; + + /// + /// A string domain primitive with both length and pattern validation attributes, as well as a custom validation method. + /// + [StringLength(1, 100)] + [Pattern(@"[A-Z]{100}",true)] + internal partial class StringWithLengthAndPattern : IDomainValue + { + /// + public static PrimitiveValidationResult Validate(string value) + { + if (value == "Test") + return "Invalid Value"; + + return PrimitiveValidationResult.Ok; + } + } + + """; + + return TestHelper.Verify(source, (_, x, _) => Assert.Equal(4, x.Count)); + } + [Fact] public Task StringValue_WithTransformerGeneratesTransformerCall() { @@ -18,7 +88,6 @@ public Task StringValue_WithTransformerGeneratesTransformerCall() namespace AltaSoft.DomainPrimitives; - /// [StringLength(1, 100)] internal partial class TransformableString : IDomainValue { diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/PrimitivesWithPatternAttributeTests.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/PrimitivesWithPatternAttributeTests.cs new file mode 100644 index 0000000..26e50e3 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/PrimitivesWithPatternAttributeTests.cs @@ -0,0 +1,48 @@ +namespace AltaSoft.DomainPrimitives.Generator.Tests; + +public class PrimitivesWithPatternAttributeTests +{ + [Theory] + [InlineData("[A-Z]{3}")] + [InlineData("[A-Z]{10}")] + [InlineData("\\d+")] + [InlineData("\\w+")] + [InlineData("\\s+")] + [InlineData("[A-Z]{3}\\d{2}")] + [InlineData("[A-Z]{3}-\\d{3}")] + [InlineData("^\\d{4}-\\d{2}-\\d{2}$")] + [InlineData("^\\w+@\\w+\\.\\w+$")] + [InlineData("^\\d{3}-\\d{2}-\\d{4}$")] + public void Pattern_Should_Compile(string pattern) + { + var source = $$""" + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using AltaSoft.DomainPrimitives; + + namespace AltaSoft.DomainPrimitives; + + /// + /// A string domain primitive with both length and pattern validation attributes, as well as a custom validation method. + /// + [StringLength(1, 100)] + [Pattern(@"{{pattern}}, true")] + internal partial class StringWithLengthAndPattern : IDomainValue + { + /// + public static PrimitiveValidationResult Validate(string value) + { + return PrimitiveValidationResult.Ok; + } + } + + """; + + TestHelper.Compile(source, (_, sources, _) => Assert.Equal(4, sources.Count)); + + } + +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#OpenApiHelper.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#OpenApiHelper.g.verified.cs new file mode 100644 index 0000000..745d0d1 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#OpenApiHelper.g.verified.cs @@ -0,0 +1,50 @@ +//HintName: OpenApiHelper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using Microsoft.OpenApi; +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +[assembly: AltaSoft.DomainPrimitives.DomainPrimitiveAssemblyAttribute] +namespace generator_Test.Converters.Helpers; + +/// +/// Helper class providing methods to configure OpenApiSchema mappings for DomainPrimitive types of generator_Test +/// +public static class OpenApiHelper +{ + /// + /// Mapping of DomainPrimitive types to OpenApiSchema definitions. + /// + /// + /// The Dictionary contains mappings for the following types: + /// + /// + /// + /// + public static FrozenDictionary Schemas = new Dictionary() + { + { + typeof(StringWithLengthAndPattern), + new OpenApiSchema + { + Type = JsonSchemaType.String, + Title = "StringWithLengthAndPattern", + Description = @"A string domain primitive with both length and pattern validation attributes, as well as a custom validation method.", + MinLength = 1, + MaxLength = 100, + Pattern = "[A-Z]{100}" + } + } + }.ToFrozenDictionary(); +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPattern.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPattern.g.verified.cs new file mode 100644 index 0000000..56d29f6 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPattern.g.verified.cs @@ -0,0 +1,328 @@ +//HintName: StringWithLengthAndPattern.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using System; +using System.Numerics; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using AltaSoft.DomainPrimitives; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using AltaSoft.DomainPrimitives.Converters; +using System.ComponentModel; + +namespace AltaSoft.DomainPrimitives; + +[JsonConverter(typeof(StringWithLengthAndPatternJsonConverter))] +[TypeConverter(typeof(StringWithLengthAndPatternTypeConverter))] +[UnderlyingPrimitiveType(typeof(string))] +[DebuggerDisplay("{_value}")] +internal partial class StringWithLengthAndPattern : IEquatable + , IComparable + , IComparable + , IParsable + , IConvertible +{ + /// + public Type GetUnderlyingPrimitiveType() => typeof(string); + /// + public object GetUnderlyingPrimitiveValue() => (string)this; + + private string _valueOrThrow => _isInitialized ? _value : throw InvalidDomainValueException.NotInitializedException(typeof(StringWithLengthAndPattern)); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _value; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly bool _isInitialized; + + /// + /// Initializes a new instance of the class by validating the specified value using static method. + /// + /// The value to be validated. + public StringWithLengthAndPattern(string value) : this(value, true) + { + } + + private StringWithLengthAndPattern(string value, bool validate) + { + if (validate) + { + if (value.Length is < 1 or > 100) + throw InvalidDomainValueException.StringRangeException(typeof(StringWithLengthAndPattern), value, 1, 100); + + ValidateOrThrow(value); + } + _value = value; + _isInitialized = true; + } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + [Obsolete("Domain primitive cannot be created using empty Constructor", true)] + public StringWithLengthAndPattern() + { + } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create StringWithLengthAndPattern from + /// When this method returns, contains the created StringWithLengthAndPattern if the conversion succeeded, or null if the conversion failed. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLengthAndPattern? result) + { + return TryCreate(value, out result, out _); + } + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create StringWithLengthAndPattern from + /// When this method returns, contains the created StringWithLengthAndPattern if the conversion succeeded, or null if the conversion failed. + /// When this method returns, contains the error message if the conversion failed; otherwise, null. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLengthAndPattern? result, [NotNullWhen(false)] out string? errorMessage) + { + if (value.Length is < 1 or > 100) + { + result = null; + errorMessage = "String length is out of range 1..100"; + return false; + } + + var validationResult = Validate(value); + if (!validationResult.IsValid) + { + result = null; + errorMessage = validationResult.ErrorMessage; + return false; + } + + result = new (value, false); + errorMessage = null; + return true; + } + + /// + /// Validates the specified value and throws an exception if it is not valid. + /// + /// The value to validate + /// Thrown when the value is not valid. + public void ValidateOrThrow(string value) + { + var result = Validate(value); + if (!result.IsValid) + throw new InvalidDomainValueException(result.ErrorMessage, typeof(StringWithLengthAndPattern), value); + } + + + /// + /// Gets the character at the specified index. + /// + public char this[int i] + { + get => _value[i]; + } + + /// + /// Gets the character at the specified index. + /// + public char this[Index index] + { + get => _value[index]; + } + + /// + /// Gets the substring by specified range. + /// + public string this[Range range] + { + get => _value[range]; + } + + /// + /// Gets the number of characters. + /// + /// The number of characters in underlying string value. + public int Length => _value.Length; + + /// + /// Returns a substring of this string. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int startIndex, int length) => _value.Substring(startIndex, length); + + /// + /// Returns a substring of this string. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int startIndex) => _value.Substring(startIndex); + + /// + /// Checks if the specified value is contained within the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(string value) => _value.Contains(value); + + /// + /// Determines whether a specified string is a prefix of the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool StartsWith(string value) => _value.StartsWith(value); + + /// + /// Determines whether a specified string is a suffix of the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool EndsWith(string value) => _value.EndsWith(value); + + /// + /// Returns the entire string as an array of characters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char[] ToCharArray() => _value.ToCharArray(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) => obj is StringWithLengthAndPattern other && Equals(other); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(StringWithLengthAndPattern? other) + { + if (other is null || !_isInitialized || !other._isInitialized) + return false; + return _value.Equals(other._value); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(StringWithLengthAndPattern? left, StringWithLengthAndPattern? right) + { + if (ReferenceEquals(left, right)) + return true; + if (left is null || right is null) + return false; + return left.Equals(right); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(StringWithLengthAndPattern? left, StringWithLengthAndPattern? right) => !(left == right); + + /// + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + + if (obj is StringWithLengthAndPattern c) + return CompareTo(c); + + throw new ArgumentException("Object is not a StringWithLengthAndPattern", nameof(obj)); + } + + /// + public int CompareTo(StringWithLengthAndPattern? other) + { + if (other is null || !other._isInitialized) + return 1; + if (!_isInitialized) + return -1; + return _value.CompareTo(other._value); + } + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator StringWithLengthAndPattern?(string? value) => value is null ? null : new(value); + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator string?(StringWithLengthAndPattern? value) => value is null ? null : (string?)value._valueOrThrow; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StringWithLengthAndPattern Parse(string s, IFormatProvider? provider) => s; + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out StringWithLengthAndPattern result) + { + if (s is null) + { + result = default; + return false; + } + + return StringWithLengthAndPattern.TryCreate(s, out result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => _valueOrThrow.GetHashCode(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + TypeCode IConvertible.GetTypeCode() => ((IConvertible)(String)_valueOrThrow).GetTypeCode(); + + /// + bool IConvertible.ToBoolean(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToBoolean(provider); + + /// + byte IConvertible.ToByte(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToByte(provider); + + /// + char IConvertible.ToChar(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToChar(provider); + + /// + DateTime IConvertible.ToDateTime(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDateTime(provider); + + /// + decimal IConvertible.ToDecimal(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDecimal(provider); + + /// + double IConvertible.ToDouble(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDouble(provider); + + /// + short IConvertible.ToInt16(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt16(provider); + + /// + int IConvertible.ToInt32(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt32(provider); + + /// + long IConvertible.ToInt64(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt64(provider); + + /// + sbyte IConvertible.ToSByte(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToSByte(provider); + + /// + float IConvertible.ToSingle(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToSingle(provider); + + /// + string IConvertible.ToString(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToString(provider); + + /// + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToType(conversionType, provider); + + /// + ushort IConvertible.ToUInt16(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt16(provider); + + /// + uint IConvertible.ToUInt32(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt32(provider); + + /// + ulong IConvertible.ToUInt64(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt64(provider); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => _valueOrThrow.ToString(); +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternJsonConverter.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternJsonConverter.g.verified.cs new file mode 100644 index 0000000..764a138 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternJsonConverter.g.verified.cs @@ -0,0 +1,62 @@ +//HintName: StringWithLengthAndPatternJsonConverter.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Globalization; +using System.Text.Json.Serialization.Metadata; + +namespace AltaSoft.DomainPrimitives.Converters; + +/// +/// JsonConverter for +/// +internal sealed class StringWithLengthAndPatternJsonConverter : JsonConverter +{ + /// + public override StringWithLengthAndPattern Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonInternalConverters.StringConverter.Read(ref reader, typeToConvert, options)!; + } + catch (InvalidDomainValueException ex) + { + throw new JsonException(ex.Message); + } + } + + /// + public override void Write(Utf8JsonWriter writer, StringWithLengthAndPattern value, JsonSerializerOptions options) + { + JsonInternalConverters.StringConverter.Write(writer, (string)value, options); + } + + /// + public override StringWithLengthAndPattern ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonInternalConverters.StringConverter.ReadAsPropertyName(ref reader, typeToConvert, options)!; + } + catch (InvalidDomainValueException ex) + { + throw new JsonException(ex.Message); + } + } + + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, StringWithLengthAndPattern value, JsonSerializerOptions options) + { + JsonInternalConverters.StringConverter.WriteAsPropertyName(writer, (string)value, options); + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternTypeConverter.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternTypeConverter.g.verified.cs new file mode 100644 index 0000000..b48242f --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternTypeConverter.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: StringWithLengthAndPatternTypeConverter.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using System; +using System.ComponentModel; +using System.Globalization; + +namespace AltaSoft.DomainPrimitives.Converters; + +/// +/// TypeConverter for +/// +internal sealed class StringWithLengthAndPatternTypeConverter : StringConverter +{ + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var result = base.ConvertFrom(context, culture, value); + if (result is null) + return null; + try + { + return new StringWithLengthAndPattern((string)result); + } + catch (InvalidDomainValueException ex) + { + throw new FormatException("Cannot parse StringWithLengthAndPattern", ex); + } + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#OpenApiHelper.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#OpenApiHelper.g.verified.cs new file mode 100644 index 0000000..745d0d1 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#OpenApiHelper.g.verified.cs @@ -0,0 +1,50 @@ +//HintName: OpenApiHelper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using Microsoft.OpenApi; +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +[assembly: AltaSoft.DomainPrimitives.DomainPrimitiveAssemblyAttribute] +namespace generator_Test.Converters.Helpers; + +/// +/// Helper class providing methods to configure OpenApiSchema mappings for DomainPrimitive types of generator_Test +/// +public static class OpenApiHelper +{ + /// + /// Mapping of DomainPrimitive types to OpenApiSchema definitions. + /// + /// + /// The Dictionary contains mappings for the following types: + /// + /// + /// + /// + public static FrozenDictionary Schemas = new Dictionary() + { + { + typeof(StringWithLengthAndPattern), + new OpenApiSchema + { + Type = JsonSchemaType.String, + Title = "StringWithLengthAndPattern", + Description = @"A string domain primitive with both length and pattern validation attributes, as well as a custom validation method.", + MinLength = 1, + MaxLength = 100, + Pattern = "[A-Z]{100}" + } + } + }.ToFrozenDictionary(); +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPattern.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPattern.g.verified.cs new file mode 100644 index 0000000..d5c77a0 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPattern.g.verified.cs @@ -0,0 +1,339 @@ +//HintName: StringWithLengthAndPattern.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using System; +using System.Numerics; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using AltaSoft.DomainPrimitives; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using AltaSoft.DomainPrimitives.Converters; +using System.ComponentModel; +using System.Text.RegularExpressions; + +namespace AltaSoft.DomainPrimitives; + +[JsonConverter(typeof(StringWithLengthAndPatternJsonConverter))] +[TypeConverter(typeof(StringWithLengthAndPatternTypeConverter))] +[UnderlyingPrimitiveType(typeof(string))] +[DebuggerDisplay("{_value}")] +internal partial class StringWithLengthAndPattern : IEquatable + , IComparable + , IComparable + , IParsable + , IConvertible +{ + /// + public Type GetUnderlyingPrimitiveType() => typeof(string); + /// + public object GetUnderlyingPrimitiveValue() => (string)this; + + private string _valueOrThrow => _isInitialized ? _value : throw InvalidDomainValueException.NotInitializedException(typeof(StringWithLengthAndPattern)); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _value; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly bool _isInitialized; + + /// + /// Initializes a new instance of the class by validating the specified value using static method. + /// + /// The value to be validated. + public StringWithLengthAndPattern(string value) : this(value, true) + { + } + + private StringWithLengthAndPattern(string value, bool validate) + { + if (validate) + { + if (value.Length is < 1 or > 100) + throw InvalidDomainValueException.StringRangeException(typeof(StringWithLengthAndPattern), value, 1, 100); + + if (!Regex.IsMatch(value, "[A-Z]{100}", RegexOptions.Compiled)) + throw InvalidDomainValueException.InvalidPatternException(typeof(StringWithLengthAndPattern), value, "[A-Z]{100}"); + + ValidateOrThrow(value); + } + _value = value; + _isInitialized = true; + } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + [Obsolete("Domain primitive cannot be created using empty Constructor", true)] + public StringWithLengthAndPattern() + { + } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create StringWithLengthAndPattern from + /// When this method returns, contains the created StringWithLengthAndPattern if the conversion succeeded, or null if the conversion failed. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLengthAndPattern? result) + { + return TryCreate(value, out result, out _); + } + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create StringWithLengthAndPattern from + /// When this method returns, contains the created StringWithLengthAndPattern if the conversion succeeded, or null if the conversion failed. + /// When this method returns, contains the error message if the conversion failed; otherwise, null. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLengthAndPattern? result, [NotNullWhen(false)] out string? errorMessage) + { + if (value.Length is < 1 or > 100) + { + result = null; + errorMessage = "String length is out of range 1..100"; + return false; + } + + if (!Regex.IsMatch(value, "[A-Z]{100}", RegexOptions.Compiled)) + { + result = null; + errorMessage = "String does not match the required pattern: " + "[A-Z]{100}"; + return false; + } + + var validationResult = Validate(value); + if (!validationResult.IsValid) + { + result = null; + errorMessage = validationResult.ErrorMessage; + return false; + } + + result = new (value, false); + errorMessage = null; + return true; + } + + /// + /// Validates the specified value and throws an exception if it is not valid. + /// + /// The value to validate + /// Thrown when the value is not valid. + public void ValidateOrThrow(string value) + { + var result = Validate(value); + if (!result.IsValid) + throw new InvalidDomainValueException(result.ErrorMessage, typeof(StringWithLengthAndPattern), value); + } + + + /// + /// Gets the character at the specified index. + /// + public char this[int i] + { + get => _value[i]; + } + + /// + /// Gets the character at the specified index. + /// + public char this[Index index] + { + get => _value[index]; + } + + /// + /// Gets the substring by specified range. + /// + public string this[Range range] + { + get => _value[range]; + } + + /// + /// Gets the number of characters. + /// + /// The number of characters in underlying string value. + public int Length => _value.Length; + + /// + /// Returns a substring of this string. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int startIndex, int length) => _value.Substring(startIndex, length); + + /// + /// Returns a substring of this string. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int startIndex) => _value.Substring(startIndex); + + /// + /// Checks if the specified value is contained within the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(string value) => _value.Contains(value); + + /// + /// Determines whether a specified string is a prefix of the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool StartsWith(string value) => _value.StartsWith(value); + + /// + /// Determines whether a specified string is a suffix of the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool EndsWith(string value) => _value.EndsWith(value); + + /// + /// Returns the entire string as an array of characters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char[] ToCharArray() => _value.ToCharArray(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) => obj is StringWithLengthAndPattern other && Equals(other); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(StringWithLengthAndPattern? other) + { + if (other is null || !_isInitialized || !other._isInitialized) + return false; + return _value.Equals(other._value); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(StringWithLengthAndPattern? left, StringWithLengthAndPattern? right) + { + if (ReferenceEquals(left, right)) + return true; + if (left is null || right is null) + return false; + return left.Equals(right); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(StringWithLengthAndPattern? left, StringWithLengthAndPattern? right) => !(left == right); + + /// + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + + if (obj is StringWithLengthAndPattern c) + return CompareTo(c); + + throw new ArgumentException("Object is not a StringWithLengthAndPattern", nameof(obj)); + } + + /// + public int CompareTo(StringWithLengthAndPattern? other) + { + if (other is null || !other._isInitialized) + return 1; + if (!_isInitialized) + return -1; + return _value.CompareTo(other._value); + } + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator StringWithLengthAndPattern?(string? value) => value is null ? null : new(value); + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator string?(StringWithLengthAndPattern? value) => value is null ? null : (string?)value._valueOrThrow; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StringWithLengthAndPattern Parse(string s, IFormatProvider? provider) => s; + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out StringWithLengthAndPattern result) + { + if (s is null) + { + result = default; + return false; + } + + return StringWithLengthAndPattern.TryCreate(s, out result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => _valueOrThrow.GetHashCode(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + TypeCode IConvertible.GetTypeCode() => ((IConvertible)(String)_valueOrThrow).GetTypeCode(); + + /// + bool IConvertible.ToBoolean(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToBoolean(provider); + + /// + byte IConvertible.ToByte(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToByte(provider); + + /// + char IConvertible.ToChar(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToChar(provider); + + /// + DateTime IConvertible.ToDateTime(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDateTime(provider); + + /// + decimal IConvertible.ToDecimal(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDecimal(provider); + + /// + double IConvertible.ToDouble(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDouble(provider); + + /// + short IConvertible.ToInt16(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt16(provider); + + /// + int IConvertible.ToInt32(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt32(provider); + + /// + long IConvertible.ToInt64(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt64(provider); + + /// + sbyte IConvertible.ToSByte(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToSByte(provider); + + /// + float IConvertible.ToSingle(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToSingle(provider); + + /// + string IConvertible.ToString(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToString(provider); + + /// + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToType(conversionType, provider); + + /// + ushort IConvertible.ToUInt16(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt16(provider); + + /// + uint IConvertible.ToUInt32(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt32(provider); + + /// + ulong IConvertible.ToUInt64(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt64(provider); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => _valueOrThrow.ToString(); +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternJsonConverter.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternJsonConverter.g.verified.cs new file mode 100644 index 0000000..764a138 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternJsonConverter.g.verified.cs @@ -0,0 +1,62 @@ +//HintName: StringWithLengthAndPatternJsonConverter.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Globalization; +using System.Text.Json.Serialization.Metadata; + +namespace AltaSoft.DomainPrimitives.Converters; + +/// +/// JsonConverter for +/// +internal sealed class StringWithLengthAndPatternJsonConverter : JsonConverter +{ + /// + public override StringWithLengthAndPattern Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonInternalConverters.StringConverter.Read(ref reader, typeToConvert, options)!; + } + catch (InvalidDomainValueException ex) + { + throw new JsonException(ex.Message); + } + } + + /// + public override void Write(Utf8JsonWriter writer, StringWithLengthAndPattern value, JsonSerializerOptions options) + { + JsonInternalConverters.StringConverter.Write(writer, (string)value, options); + } + + /// + public override StringWithLengthAndPattern ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonInternalConverters.StringConverter.ReadAsPropertyName(ref reader, typeToConvert, options)!; + } + catch (InvalidDomainValueException ex) + { + throw new JsonException(ex.Message); + } + } + + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, StringWithLengthAndPattern value, JsonSerializerOptions options) + { + JsonInternalConverters.StringConverter.WriteAsPropertyName(writer, (string)value, options); + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternTypeConverter.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternTypeConverter.g.verified.cs new file mode 100644 index 0000000..b48242f --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternTypeConverter.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: StringWithLengthAndPatternTypeConverter.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using System; +using System.ComponentModel; +using System.Globalization; + +namespace AltaSoft.DomainPrimitives.Converters; + +/// +/// TypeConverter for +/// +internal sealed class StringWithLengthAndPatternTypeConverter : StringConverter +{ + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var result = base.ConvertFrom(context, culture, value); + if (result is null) + return null; + try + { + return new StringWithLengthAndPattern((string)result); + } + catch (InvalidDomainValueException ex) + { + throw new FormatException("Cannot parse StringWithLengthAndPattern", ex); + } + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#OpenApiHelper.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#OpenApiHelper.g.verified.cs new file mode 100644 index 0000000..745d0d1 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#OpenApiHelper.g.verified.cs @@ -0,0 +1,50 @@ +//HintName: OpenApiHelper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using Microsoft.OpenApi; +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Text.Json.Nodes; + +[assembly: AltaSoft.DomainPrimitives.DomainPrimitiveAssemblyAttribute] +namespace generator_Test.Converters.Helpers; + +/// +/// Helper class providing methods to configure OpenApiSchema mappings for DomainPrimitive types of generator_Test +/// +public static class OpenApiHelper +{ + /// + /// Mapping of DomainPrimitive types to OpenApiSchema definitions. + /// + /// + /// The Dictionary contains mappings for the following types: + /// + /// + /// + /// + public static FrozenDictionary Schemas = new Dictionary() + { + { + typeof(StringWithLengthAndPattern), + new OpenApiSchema + { + Type = JsonSchemaType.String, + Title = "StringWithLengthAndPattern", + Description = @"A string domain primitive with both length and pattern validation attributes, as well as a custom validation method.", + MinLength = 1, + MaxLength = 100, + Pattern = "[A-Z]{100}" + } + } + }.ToFrozenDictionary(); +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPattern.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPattern.g.verified.cs new file mode 100644 index 0000000..56d29f6 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPattern.g.verified.cs @@ -0,0 +1,328 @@ +//HintName: StringWithLengthAndPattern.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using System; +using System.Numerics; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using AltaSoft.DomainPrimitives; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using AltaSoft.DomainPrimitives.Converters; +using System.ComponentModel; + +namespace AltaSoft.DomainPrimitives; + +[JsonConverter(typeof(StringWithLengthAndPatternJsonConverter))] +[TypeConverter(typeof(StringWithLengthAndPatternTypeConverter))] +[UnderlyingPrimitiveType(typeof(string))] +[DebuggerDisplay("{_value}")] +internal partial class StringWithLengthAndPattern : IEquatable + , IComparable + , IComparable + , IParsable + , IConvertible +{ + /// + public Type GetUnderlyingPrimitiveType() => typeof(string); + /// + public object GetUnderlyingPrimitiveValue() => (string)this; + + private string _valueOrThrow => _isInitialized ? _value : throw InvalidDomainValueException.NotInitializedException(typeof(StringWithLengthAndPattern)); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _value; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly bool _isInitialized; + + /// + /// Initializes a new instance of the class by validating the specified value using static method. + /// + /// The value to be validated. + public StringWithLengthAndPattern(string value) : this(value, true) + { + } + + private StringWithLengthAndPattern(string value, bool validate) + { + if (validate) + { + if (value.Length is < 1 or > 100) + throw InvalidDomainValueException.StringRangeException(typeof(StringWithLengthAndPattern), value, 1, 100); + + ValidateOrThrow(value); + } + _value = value; + _isInitialized = true; + } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + [Obsolete("Domain primitive cannot be created using empty Constructor", true)] + public StringWithLengthAndPattern() + { + } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create StringWithLengthAndPattern from + /// When this method returns, contains the created StringWithLengthAndPattern if the conversion succeeded, or null if the conversion failed. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLengthAndPattern? result) + { + return TryCreate(value, out result, out _); + } + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create StringWithLengthAndPattern from + /// When this method returns, contains the created StringWithLengthAndPattern if the conversion succeeded, or null if the conversion failed. + /// When this method returns, contains the error message if the conversion failed; otherwise, null. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLengthAndPattern? result, [NotNullWhen(false)] out string? errorMessage) + { + if (value.Length is < 1 or > 100) + { + result = null; + errorMessage = "String length is out of range 1..100"; + return false; + } + + var validationResult = Validate(value); + if (!validationResult.IsValid) + { + result = null; + errorMessage = validationResult.ErrorMessage; + return false; + } + + result = new (value, false); + errorMessage = null; + return true; + } + + /// + /// Validates the specified value and throws an exception if it is not valid. + /// + /// The value to validate + /// Thrown when the value is not valid. + public void ValidateOrThrow(string value) + { + var result = Validate(value); + if (!result.IsValid) + throw new InvalidDomainValueException(result.ErrorMessage, typeof(StringWithLengthAndPattern), value); + } + + + /// + /// Gets the character at the specified index. + /// + public char this[int i] + { + get => _value[i]; + } + + /// + /// Gets the character at the specified index. + /// + public char this[Index index] + { + get => _value[index]; + } + + /// + /// Gets the substring by specified range. + /// + public string this[Range range] + { + get => _value[range]; + } + + /// + /// Gets the number of characters. + /// + /// The number of characters in underlying string value. + public int Length => _value.Length; + + /// + /// Returns a substring of this string. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int startIndex, int length) => _value.Substring(startIndex, length); + + /// + /// Returns a substring of this string. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string Substring(int startIndex) => _value.Substring(startIndex); + + /// + /// Checks if the specified value is contained within the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(string value) => _value.Contains(value); + + /// + /// Determines whether a specified string is a prefix of the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool StartsWith(string value) => _value.StartsWith(value); + + /// + /// Determines whether a specified string is a suffix of the current instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool EndsWith(string value) => _value.EndsWith(value); + + /// + /// Returns the entire string as an array of characters. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char[] ToCharArray() => _value.ToCharArray(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) => obj is StringWithLengthAndPattern other && Equals(other); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(StringWithLengthAndPattern? other) + { + if (other is null || !_isInitialized || !other._isInitialized) + return false; + return _value.Equals(other._value); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(StringWithLengthAndPattern? left, StringWithLengthAndPattern? right) + { + if (ReferenceEquals(left, right)) + return true; + if (left is null || right is null) + return false; + return left.Equals(right); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(StringWithLengthAndPattern? left, StringWithLengthAndPattern? right) => !(left == right); + + /// + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + + if (obj is StringWithLengthAndPattern c) + return CompareTo(c); + + throw new ArgumentException("Object is not a StringWithLengthAndPattern", nameof(obj)); + } + + /// + public int CompareTo(StringWithLengthAndPattern? other) + { + if (other is null || !other._isInitialized) + return 1; + if (!_isInitialized) + return -1; + return _value.CompareTo(other._value); + } + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator StringWithLengthAndPattern?(string? value) => value is null ? null : new(value); + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator string?(StringWithLengthAndPattern? value) => value is null ? null : (string?)value._valueOrThrow; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StringWithLengthAndPattern Parse(string s, IFormatProvider? provider) => s; + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out StringWithLengthAndPattern result) + { + if (s is null) + { + result = default; + return false; + } + + return StringWithLengthAndPattern.TryCreate(s, out result); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => _valueOrThrow.GetHashCode(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + TypeCode IConvertible.GetTypeCode() => ((IConvertible)(String)_valueOrThrow).GetTypeCode(); + + /// + bool IConvertible.ToBoolean(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToBoolean(provider); + + /// + byte IConvertible.ToByte(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToByte(provider); + + /// + char IConvertible.ToChar(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToChar(provider); + + /// + DateTime IConvertible.ToDateTime(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDateTime(provider); + + /// + decimal IConvertible.ToDecimal(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDecimal(provider); + + /// + double IConvertible.ToDouble(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToDouble(provider); + + /// + short IConvertible.ToInt16(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt16(provider); + + /// + int IConvertible.ToInt32(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt32(provider); + + /// + long IConvertible.ToInt64(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToInt64(provider); + + /// + sbyte IConvertible.ToSByte(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToSByte(provider); + + /// + float IConvertible.ToSingle(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToSingle(provider); + + /// + string IConvertible.ToString(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToString(provider); + + /// + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToType(conversionType, provider); + + /// + ushort IConvertible.ToUInt16(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt16(provider); + + /// + uint IConvertible.ToUInt32(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt32(provider); + + /// + ulong IConvertible.ToUInt64(IFormatProvider? provider) => ((IConvertible)(String)_valueOrThrow).ToUInt64(provider); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => _valueOrThrow.ToString(); +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternJsonConverter.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternJsonConverter.g.verified.cs new file mode 100644 index 0000000..764a138 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternJsonConverter.g.verified.cs @@ -0,0 +1,62 @@ +//HintName: StringWithLengthAndPatternJsonConverter.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Globalization; +using System.Text.Json.Serialization.Metadata; + +namespace AltaSoft.DomainPrimitives.Converters; + +/// +/// JsonConverter for +/// +internal sealed class StringWithLengthAndPatternJsonConverter : JsonConverter +{ + /// + public override StringWithLengthAndPattern Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonInternalConverters.StringConverter.Read(ref reader, typeToConvert, options)!; + } + catch (InvalidDomainValueException ex) + { + throw new JsonException(ex.Message); + } + } + + /// + public override void Write(Utf8JsonWriter writer, StringWithLengthAndPattern value, JsonSerializerOptions options) + { + JsonInternalConverters.StringConverter.Write(writer, (string)value, options); + } + + /// + public override StringWithLengthAndPattern ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return JsonInternalConverters.StringConverter.ReadAsPropertyName(ref reader, typeToConvert, options)!; + } + catch (InvalidDomainValueException ex) + { + throw new JsonException(ex.Message); + } + } + + /// + public override void WriteAsPropertyName(Utf8JsonWriter writer, StringWithLengthAndPattern value, JsonSerializerOptions options) + { + JsonInternalConverters.StringConverter.WriteAsPropertyName(writer, (string)value, options); + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternTypeConverter.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternTypeConverter.g.verified.cs new file mode 100644 index 0000000..b48242f --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternTypeConverter.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: StringWithLengthAndPatternTypeConverter.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using AltaSoft.DomainPrimitives; +using System; +using System.ComponentModel; +using System.Globalization; + +namespace AltaSoft.DomainPrimitives.Converters; + +/// +/// TypeConverter for +/// +internal sealed class StringWithLengthAndPatternTypeConverter : StringConverter +{ + /// + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + var result = base.ConvertFrom(context, culture, value); + if (result is null) + return null; + try + { + return new StringWithLengthAndPattern((string)result); + } + catch (InvalidDomainValueException ex) + { + throw new FormatException("Cannot parse StringWithLengthAndPattern", ex); + } + } +} diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransformerGeneratesTransformerCall#OpenApiHelper.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransformerGeneratesTransformerCall#OpenApiHelper.g.verified.cs index 32ac159..6818420 100644 --- a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransformerGeneratesTransformerCall#OpenApiHelper.g.verified.cs +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransformerGeneratesTransformerCall#OpenApiHelper.g.verified.cs @@ -39,7 +39,9 @@ public static class OpenApiHelper new OpenApiSchema { Type = JsonSchemaType.String, - Title = "TransformableString" + Title = "TransformableString", + MinLength = 1, + MaxLength = 100 } } }.ToFrozenDictionary(); diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelper.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelper.cs index 7654d46..79a420c 100644 --- a/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelper.cs +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/TestHelper.cs @@ -5,19 +5,24 @@ using Microsoft.CodeAnalysis; using Microsoft.OpenApi; -namespace AltaSoft.DomainPrimitives.Generator.Tests +namespace AltaSoft.DomainPrimitives.Generator.Tests; + +public static class TestHelper { - public static class TestHelper + internal static Task Verify(string source, Action, List, GeneratorDriver>? additionalChecks = null, DomainPrimitiveGlobalOptions? options = null) { - internal static Task Verify(string source, Action, List, GeneratorDriver>? additionalChecks = null, DomainPrimitiveGlobalOptions? options = null) - { - List assemblies = [typeof(JsonSerializer).Assembly, typeof(OpenApiSchema).Assembly]; - var (diagnostics, output, driver) = TestHelpers.GetGeneratedOutput(source, assemblies, options); + var driver = Compile(source, additionalChecks, options); + + return Verifier.Verify(driver).UseDirectory("Snapshots"); + } - Assert.Empty(diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error)); - additionalChecks?.Invoke(diagnostics, output, driver); + internal static GeneratorDriver Compile(string source, Action, List, GeneratorDriver>? additionalChecks, DomainPrimitiveGlobalOptions? options = null) + { + List assemblies = [typeof(JsonSerializer).Assembly, typeof(OpenApiSchema).Assembly]; + var (diagnostics, output, driver) = TestHelpers.GetGeneratedOutput(source, assemblies, options); - return Verifier.Verify(driver).UseDirectory("Snapshots"); - } + Assert.Empty(diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error)); + additionalChecks?.Invoke(diagnostics, output, driver); + return driver; } } diff --git a/tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedString.cs b/tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedString.cs new file mode 100644 index 0000000..14bf8df --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedString.cs @@ -0,0 +1,13 @@ +namespace AltaSoft.DomainPrimitives.UnitTests.TransformableTests; + +/// +/// Pattern based string +/// +[Pattern("^[a-zA-H]{5,10}$", true)] +public sealed partial class PatternBasedString : IDomainValue +{ + public static PrimitiveValidationResult Validate(string value) + { + return PrimitiveValidationResult.Ok; + } +} diff --git a/tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedStringTests.cs b/tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedStringTests.cs new file mode 100644 index 0000000..8923d9d --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedStringTests.cs @@ -0,0 +1,107 @@ +using Xunit; + +namespace AltaSoft.DomainPrimitives.UnitTests.TransformableTests; + +public class PatternBasedStringTests +{ + [Theory] + [InlineData("ABCDE")] + [InlineData("Hebboo")] + [InlineData("ABCDEFGH")] + [InlineData("ABCDEFGHAB")] + public void Constructor_WithValidValue_CreatesInstance(string value) + { + var result = new PatternBasedString(value); + + Assert.NotNull(result); + Assert.Equal(value, (string)result); + } + + [Theory] + [InlineData("ABC")] + [InlineData("ABCD")] + public void Constructor_WithValueTooShort_ThrowsInvalidDomainValueException(string value) + { + var exception = Assert.Throws(() => new PatternBasedString(value)); + + Assert.NotNull(exception); + } + + [Theory] + [InlineData("ABCDEFGHIJK")] + [InlineData("ABCDEFGHIJKLMNOP")] + public void Constructor_WithValueTooLong_ThrowsInvalidDomainValueException(string value) + { + var exception = Assert.Throws(() => new PatternBasedString(value)); + + Assert.NotNull(exception); + } + + [Theory] + [InlineData("ABCDI")] + [InlineData("ABCDJ")] + [InlineData("ABCDZ")] + [InlineData("HELLO1")] + [InlineData("ABC@E")] + [InlineData("ABC DE")] + public void Constructor_WithInvalidCharacters_ThrowsInvalidDomainValueException(string value) + { + var exception = Assert.Throws(() => new PatternBasedString(value)); + + Assert.NotNull(exception); + } + + [Theory] + [InlineData("ABCDE")] + [InlineData("CEBBAx")] + [InlineData("ABCDEFGH")] + public void TryCreate_WithValidValue_ReturnsTrue(string value) + { + var result = PatternBasedString.TryCreate(value, out var patternString); + + Assert.True(result); + Assert.NotNull(patternString); + Assert.Equal(value, (string)patternString); + } + + [Theory] + [InlineData("ABC")] + [InlineData("ABCDEFGHIJK")] + [InlineData("1abcde")] + [InlineData("ABCDI")] + [InlineData("ABC@E")] + public void TryCreate_WithInvalidValue_ReturnsFalse(string value) + { + var result = PatternBasedString.TryCreate(value, out var patternString); + + Assert.False(result); + Assert.Null(patternString); + } + + [Fact] + public void ImplicitCast_FromValidString_CreatesInstance() + { + PatternBasedString result = "aabccH"; + + Assert.NotNull(result); + Assert.Equal("aabccH", (string)result); + } + + [Fact] + public void ImplicitCast_FromInvalidString_ThrowsInvalidDomainValueException() + { + Assert.Throws(() => + { + PatternBasedString result = "1invalid"; + }); + } + + [Fact] + public void ImplicitCast_FromNull_ReturnsNull() + { + PatternBasedString? result = (string?)null; + + Assert.Null(result); + } + +}