From 3f2256905f89d5db1c06423fef7a78b420b5fe35 Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Wed, 4 Mar 2026 15:56:06 +0400 Subject: [PATCH 1/5] Added Pattern attribute support for string domain primitives. Generated OpenAPI schemas now include pattern, and optional runtime regex validation was implemented. Added related tests and error handling for pattern mismatches. Bumped version to 8.0.1. --- Directory.Build.props | 2 +- .../Customer.cs | 3 +- .../Executor.cs | 39 ++ .../Helpers/Constants.cs | 1 + .../Helpers/MethodGeneratorHelper.cs | 31 ++ .../Models/GeneratorData.cs | 11 + .../InvalidDomainValueException.cs | 13 + .../PatternAttribute.cs | 37 ++ .../DomainPrimitiveGeneratorTest.cs | 71 +++- .../PrimitivesWithPatternAttributeTests.cs | 48 +++ ...ngthAndPattern#OpenApiHelper.g.verified.cs | 50 +++ ...n#StringWithLengthAndPattern.g.verified.cs | 328 +++++++++++++++++ ...engthAndPatternJsonConverter.g.verified.cs | 62 ++++ ...engthAndPatternTypeConverter.g.verified.cs | 38 ++ ...WithValidation#OpenApiHelper.g.verified.cs | 50 +++ ...n#StringWithLengthAndPattern.g.verified.cs | 339 ++++++++++++++++++ ...engthAndPatternJsonConverter.g.verified.cs | 62 ++++ ...engthAndPatternTypeConverter.g.verified.cs | 38 ++ ...hAndXmlSummary#OpenApiHelper.g.verified.cs | 50 +++ ...y#StringWithLengthAndPattern.g.verified.cs | 328 +++++++++++++++++ ...engthAndPatternJsonConverter.g.verified.cs | 62 ++++ ...engthAndPatternTypeConverter.g.verified.cs | 38 ++ ...ransformerCall#OpenApiHelper.g.verified.cs | 4 +- .../TestHelper.cs | 25 +- .../TransformableTests/PatternBasedString.cs | 13 + .../PatternBasedStringTests.cs | 107 ++++++ 26 files changed, 1836 insertions(+), 14 deletions(-) create mode 100644 src/AltaSoft.DomainPrimitives/PatternAttribute.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/PrimitivesWithPatternAttributeTests.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#OpenApiHelper.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPattern.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternJsonConverter.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPattern#StringWithLengthAndPatternTypeConverter.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#OpenApiHelper.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPattern.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternJsonConverter.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndPatternWithValidation#StringWithLengthAndPatternTypeConverter.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#OpenApiHelper.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPattern.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternJsonConverter.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.StringValue_WithTransStringLengthAndXmlSummary#StringWithLengthAndPatternTypeConverter.g.verified.cs create mode 100644 tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedString.cs create mode 100644 tests/AltaSoft.DomainPrimitives.UnitTests/TransformableTests/PatternBasedStringTests.cs 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..9015fae 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,23 @@ 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; + + sb.AppendLine($"if (!Regex.IsMatch(value, {QuoteAndEscape(data.Pattern)}, RegexOptions.Compiled))") + .OpenBracket() + .AppendLine("result = null;") + .AppendLine($"errorMessage = \"String does not match the required pattern: {data.Pattern}\";") + .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..c2fe7b4 --- /dev/null +++ b/src/AltaSoft.DomainPrimitives/PatternAttribute.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AltaSoft.DomainPrimitives; + +/// +/// Specifies a regex pattern for OpenAPI schema generation only. +/// By default, the pattern is not validated at runtime; validation occurs only when explicitly requested. +/// +[AttributeUsage(AttributeTargets.Class)] +public class PatternAttribute : Attribute +{ + /// + /// Gets the regex pattern used in the generated OpenAPI schema. + /// + public string Pattern { get; } + + /// + /// Gets a value indicating whether runtime validation should be performed. + /// + public bool Validate { get; } + + /// + /// Initializes a new instance of . + /// + /// The regex pattern used for OpenAPI schema generation. + /// + /// A value indicating whether runtime validation should be performed. 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..fa860b2 --- /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); + } + +} From 0c66c3f1fbc5ee756af73a9cc9dde1bebff1d0b4 Mon Sep 17 00:00:00 2001 From: Teimuraz Nikolaishvili <47372530+temonk@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:02:51 +0400 Subject: [PATCH 2/5] Update src/AltaSoft.DomainPrimitives/PatternAttribute.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/AltaSoft.DomainPrimitives/PatternAttribute.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/AltaSoft.DomainPrimitives/PatternAttribute.cs b/src/AltaSoft.DomainPrimitives/PatternAttribute.cs index c2fe7b4..de6e351 100644 --- a/src/AltaSoft.DomainPrimitives/PatternAttribute.cs +++ b/src/AltaSoft.DomainPrimitives/PatternAttribute.cs @@ -4,28 +4,30 @@ namespace AltaSoft.DomainPrimitives; /// -/// Specifies a regex pattern for OpenAPI schema generation only. -/// By default, the pattern is not validated at runtime; validation occurs only when explicitly requested. +/// 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. + /// 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 runtime validation should be performed. + /// 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 used for OpenAPI schema generation. + /// + /// 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. Defaults to + /// A value indicating whether runtime validation should be performed using . Defaults to /// to avoid incurring runtime validation overhead unless explicitly requested. /// From 3536b4fe1159af0209e9438ddc3a7e2de88e1923 Mon Sep 17 00:00:00 2001 From: Teimuraz Nikolaishvili <47372530+temonk@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:03:25 +0400 Subject: [PATCH 3/5] Update src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Helpers/MethodGeneratorHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs index 9015fae..c2b9273 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs @@ -503,7 +503,7 @@ static void AddPatternValidation(GeneratorData data, SourceCodeBuilder sb) sb.AppendLine($"if (!Regex.IsMatch(value, {QuoteAndEscape(data.Pattern)}, RegexOptions.Compiled))") .OpenBracket() .AppendLine("result = null;") - .AppendLine($"errorMessage = \"String does not match the required pattern: {data.Pattern}\";") + .AppendLine($"errorMessage = \"String does not match the required pattern: \" + {QuoteAndEscape(data.Pattern)};") .AppendLine("return false;") .CloseBracket() .NewLine(); From f51dadc3782f585cd48567cb7ab4dc1225c78aa1 Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Wed, 4 Mar 2026 16:11:19 +0400 Subject: [PATCH 4/5] Copilot suggestions --- .../Helpers/MethodGeneratorHelper.cs | 5 +++-- .../InvalidDomainValueException.cs | 6 +++--- ...nWithValidation#StringWithLengthAndPattern.g.verified.cs | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs index c2b9273..238458a 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs @@ -500,10 +500,11 @@ static void AddPatternValidation(GeneratorData data, SourceCodeBuilder sb) if (!data.ValidatePattern) return; - sb.AppendLine($"if (!Regex.IsMatch(value, {QuoteAndEscape(data.Pattern)}, RegexOptions.Compiled))") + 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: \" + {QuoteAndEscape(data.Pattern)};") + .AppendLine($"errorMessage = \"String does not match the required pattern: \" + {quoted};") .AppendLine("return false;") .CloseBracket() .NewLine(); diff --git a/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs b/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs index 02ff306..ae3a2e9 100644 --- a/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs +++ b/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs @@ -73,12 +73,12 @@ public static InvalidDomainValueException LimitExceededException(Type type, int /// /// The of the domain primitive. /// The string value that failed to match the pattern. - /// The expected regex pattern. + /// The expected regex pattern. /// An describing the pattern mismatch. [EditorBrowsable(EditorBrowsableState.Never)] - public static InvalidDomainValueException InvalidPatternException(Type type, string value, string pattern) + public static InvalidDomainValueException InvalidPatternException(Type type, string value, string quotedPattern) { - return new InvalidDomainValueException($"String value does not match the required pattern '{pattern}'", type, value); + return new InvalidDomainValueException($"String value does not match the required pattern '{quotedPattern}'", type, value); } /// 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 index fa860b2..d5c77a0 100644 --- 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 @@ -104,7 +104,7 @@ public static bool TryCreate(string value, [NotNullWhen(true)] out StringWithLe if (!Regex.IsMatch(value, "[A-Z]{100}", RegexOptions.Compiled)) { result = null; - errorMessage = "String does not match the required pattern: [A-Z]{100}"; + errorMessage = "String does not match the required pattern: " + "[A-Z]{100}"; return false; } From a82093aef979c9f1ce5612b7fc377c04170b6893 Mon Sep 17 00:00:00 2001 From: Teimuraz Nikolaishvili <47372530+temonk@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:18:39 +0400 Subject: [PATCH 5/5] Update src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../InvalidDomainValueException.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs b/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs index ae3a2e9..02ff306 100644 --- a/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs +++ b/src/AltaSoft.DomainPrimitives/InvalidDomainValueException.cs @@ -73,12 +73,12 @@ public static InvalidDomainValueException LimitExceededException(Type type, int /// /// The of the domain primitive. /// The string value that failed to match the pattern. - /// The expected regex pattern. + /// The expected regex pattern. /// An describing the pattern mismatch. [EditorBrowsable(EditorBrowsableState.Never)] - public static InvalidDomainValueException InvalidPatternException(Type type, string value, string quotedPattern) + public static InvalidDomainValueException InvalidPatternException(Type type, string value, string pattern) { - return new InvalidDomainValueException($"String value does not match the required pattern '{quotedPattern}'", type, value); + return new InvalidDomainValueException($"String value does not match the required pattern '{pattern}'", type, value); } ///