diff --git a/.editorconfig b/.editorconfig index 7412fd9..d733326 100644 --- a/.editorconfig +++ b/.editorconfig @@ -87,3 +87,193 @@ indent_style = tab [*.{received,verified}.txt] insert_final_newline = false trim_trailing_whitespace = false + +[*.{cs,csx,vb,vbx}] +# .NET Code Style Settings +# See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Don't use 'this.'/'Me.' prefix for anything +dotnet_style_qualification_for_field = false : error +dotnet_style_qualification_for_property = false : error +dotnet_style_qualification_for_method = false : error +dotnet_style_qualification_for_event = false : error + +# Use language keywords over framework type names for type references +# i.e. prefer 'string' over 'String' +dotnet_style_predefined_type_for_locals_parameters_members = true : error +dotnet_style_predefined_type_for_member_access = true : error + +# Prefer object/collection initializers +# This is a suggestion because there are cases where this is necessary +dotnet_style_object_initializer = true : warning +dotnet_style_collection_initializer = true : warning + +# C# 7: Prefer using named tuple names over '.Item1', '.Item2', etc. +dotnet_style_explicit_tuple_names = true : error + +# Prefer using 'foo ?? bar' over 'foo is not null ? foo : bar' +dotnet_style_coalesce_expression = true : error + +# Prefer using '?.' over ternary null checking where possible +dotnet_style_null_propagation = true : error + +# Modifier preferences +# See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers +dotnet_style_require_accessibility_modifiers = always : error +dotnet_style_readonly_field = true : warning + +# Required Styles +dotnet_naming_style.all_const.capitalization = pascal_case +dotnet_naming_symbols.all_const.applicable_kinds = field +dotnet_naming_symbols.all_const.required_modifiers = const +dotnet_naming_rule.all_const.severity = error +dotnet_naming_rule.all_const.style = all_elements +dotnet_naming_rule.all_const.symbols = all_const + +dotnet_naming_style.all_fields.required_prefix = _ +dotnet_naming_style.all_fields.capitalization = camel_case +dotnet_naming_symbols.all_fields.applicable_kinds = field +dotnet_naming_rule.all_fields.severity = error +dotnet_naming_rule.all_fields.style = all_fields +dotnet_naming_rule.all_fields.symbols = all_fields + +dotnet_naming_style.all_interfaces.required_prefix = I +dotnet_naming_style.all_interfaces.capitalization = pascal_case +dotnet_naming_symbols.all_interfaces.applicable_kinds = interface +dotnet_naming_rule.all_interfaces.severity = error +dotnet_naming_rule.all_interfaces.style = all_interfaces +dotnet_naming_rule.all_interfaces.symbols = all_interfaces + +dotnet_naming_style.all_type_parameter.required_prefix = T +dotnet_naming_style.all_type_parameter.capitalization = pascal_case +dotnet_naming_symbols.all_type_parameter.applicable_kinds = type_parameter +dotnet_naming_rule.all_type_parameter.severity = error +dotnet_naming_rule.all_type_parameter.style = all_type_parameter +dotnet_naming_rule.all_type_parameter.symbols = all_type_parameter + +dotnet_naming_style.abstract_class.required_suffix = Base +dotnet_naming_style.abstract_class.capitalization = pascal_case +dotnet_naming_symbols.abstract_class.applicable_kinds = class +dotnet_naming_symbols.abstract_class.required_modifiers = abstract +dotnet_naming_rule.abstract_class.severity = warning +dotnet_naming_rule.abstract_class.style = abstract_class +dotnet_naming_rule.abstract_class.symbols = abstract_class + +dotnet_naming_style.method_async.required_suffix = Async +dotnet_naming_style.method_async.capitalization = pascal_case +dotnet_naming_symbols.method_async.applicable_kinds = method +dotnet_naming_symbols.method_async.required_modifiers = async +dotnet_naming_rule.method_async.severity = warning +dotnet_naming_rule.method_async.style = method_async +dotnet_naming_rule.method_async.symbols = method_async + +dotnet_naming_style.all_elements.capitalization = pascal_case +dotnet_naming_symbols.all_elements.applicable_kinds = namespace,class,struct,enum,property,method,event,delegate,local_function +dotnet_naming_rule.all_elements.severity = error +dotnet_naming_rule.all_elements.style = all_elements +dotnet_naming_rule.all_elements.symbols = all_elements + +dotnet_naming_style.all_parameters.capitalization = camel_case +dotnet_naming_symbols.all_parameters.applicable_kinds = parameter,local +dotnet_naming_rule.all_parameters.severity = error +dotnet_naming_rule.all_parameters.style = all_parameters +dotnet_naming_rule.all_parameters.symbols = all_parameters + +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_is_null_check_over_reference_equality_method = true : suggestion +dotnet_style_prefer_auto_properties = true : silent + +# Placement for using directives +csharp_using_directive_placement = inside_namespace : warning + +# Use 'var' in all cases where it can be used +csharp_style_var_for_built_in_types = true : error +csharp_style_var_when_type_is_apparent = true : error +csharp_style_var_elsewhere = true : error + +# Unused value preferences +csharp_style_unused_value_expression_statement_preference = discard_variable : warning +csharp_style_unused_value_assignment_preference = discard_variable : warning + +# C# 7: Prefer using pattern matching over "if(x is T) { var t = (T)x; }" and "var t = x as T; if(t is not null) { ... }" +csharp_style_pattern_matching_over_is_with_cast_check = true : warning +csharp_style_pattern_matching_over_as_with_null_check = true : warning + +# C# 7: Prefer using 'out var' where possible +csharp_style_inlined_variable_declaration = true : error + +# C# 7: Use throw expressions when null-checking +csharp_style_throw_expression = false : error + +# Prefer using "func?.Invoke(args)" over "if(func is not null) { func(args); }" +csharp_style_conditional_delegate_call = true : error + +# Newline settings +csharp_indent_braces = false +csharp_open_brace_on_new_line = all +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Prefer expression-bodied methods, constructors, operators, etc. +csharp_style_expression_bodied_methods = true : warning +csharp_style_expression_bodied_constructors = true : warning +csharp_style_expression_bodied_operators = true : warning +csharp_style_expression_bodied_properties = true : warning +csharp_style_expression_bodied_indexers = true : warning +csharp_style_expression_bodied_accessors = true : warning + +# Prefer Braces even for one line of code, because of +csharp_prefer_braces = true : error +csharp_type_declaration_braces = next_line +csharp_invocable_declaration_braces = next_line +csharp_anonymous_method_declaration_braces = next_line +csharp_accessor_owner_declaration_braces = next_line +csharp_accessor_declaration_braces = next_line +csharp_case_block_braces = next_line +csharp_initializer_braces = next_line +csharp_other_braces = next_line + +# Tuple Preferences +csharp_style_deconstructed_variable_declaration = true : warning + +# Simplify new expression (IDE0090) +csharp_style_implicit_object_creation_when_type_is_apparent = false +csharp_style_namespace_declarations = file_scoped : warning +csharp_prefer_simple_using_statement = false : suggestion +csharp_indent_labels = one_less_than_current +csharp_style_expression_bodied_lambdas = true : silent +csharp_style_expression_bodied_local_functions = false : silent + +# Use Compound assignment +dotnet_style_prefer_compound_assignment = false + +# Prefer if-else statement +dotnet_style_prefer_conditional_expression_over_return = false +dotnet_diagnostic.IDE0046.severity = suggestion + +# Prefer standard constructors +csharp_style_prefer_primary_constructors = false +dotnet_diagnostic.IDE0290.severity = suggestion + +# [CSharpier] Incompatible rules deactivated +# https://csharpier.com/docs/IntegratingWithLinters#code-analysis-rules +dotnet_diagnostic.IDE0055.severity = none +dotnet_diagnostic.SA1000.severity = none +dotnet_diagnostic.SA1009.severity = none +dotnet_diagnostic.SA1111.severity = none +dotnet_diagnostic.SA1118.severity = none +dotnet_diagnostic.SA1137.severity = none +dotnet_diagnostic.SA1413.severity = none +dotnet_diagnostic.SA1500.severity = none +dotnet_diagnostic.SA1501.severity = none +dotnet_diagnostic.SA1502.severity = none +dotnet_diagnostic.SA1504.severity = none +dotnet_diagnostic.SA1515.severity = none +dotnet_diagnostic.SA1516.severity = none + diff --git a/Directory.Build.props b/Directory.Build.props index 41c6bd1..a6ab860 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,14 +1,5 @@ - - $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', 'eng')) - $([MSBuild]::NormalizeDirectory('$(DirEngineering)', 'settings')) - - - - - - $(MSBuildProjectName) The fluent value validation library provides a set of fluent interfaces to validate values. @@ -20,8 +11,8 @@ - net6.0;net8.0 - net6.0;net7.0;net8.0 + net6.0;net8.0;net9.0 + net6.0;net7.0;net8.0;net9.0 diff --git a/Directory.Build.targets b/Directory.Build.targets deleted file mode 100644 index a54343c..0000000 --- a/Directory.Build.targets +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 3ade662..bcd64c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + diff --git a/Directory.Solution.props b/Directory.Solution.props index ae1cea1..ebd67c8 100644 --- a/Directory.Solution.props +++ b/Directory.Solution.props @@ -1,10 +1,6 @@ - - $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', 'eng')) - $([MSBuild]::NormalizeDirectory('$(DirEngineering)', 'settings')) + + true - - - diff --git a/FluentValue.sln b/FluentValue.sln index ddae35a..92cefdf 100644 --- a/FluentValue.sln +++ b/FluentValue.sln @@ -22,7 +22,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore .gitmodules = .gitmodules Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets Directory.Packages.props = Directory.Packages.props Directory.Solution.props = Directory.Solution.props GitVersion.yml = GitVersion.yml diff --git a/logo.png b/logo.png index 282c2ea..ee6ec09 100644 Binary files a/logo.png and b/logo.png differ diff --git a/src/NetEvolve.FluentValue/Constraints/ContainsConstraint.cs b/src/NetEvolve.FluentValue/Constraints/ContainsConstraint.cs index f49e1b0..bb2249b 100644 --- a/src/NetEvolve.FluentValue/Constraints/ContainsConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/ContainsConstraint.cs @@ -10,13 +10,13 @@ internal sealed class ContainsConstraint : ConstraintBase private readonly object? _compareValue; private readonly StringComparison? _comparison; - public ContainsConstraint(char compareValue, StringComparison comparison) + internal ContainsConstraint(char compareValue, StringComparison comparison) { _compareValue = compareValue; _comparison = comparison; } - public ContainsConstraint(string compareValue, StringComparison comparison) + internal ContainsConstraint(string compareValue, StringComparison comparison) { _compareValue = compareValue; _comparison = comparison; @@ -37,7 +37,7 @@ public override bool IsSatisfiedBy(object? value) => _comparison ?? default ), IDictionary dictionary => dictionary.Contains(_compareValue!), - IList list => list.Contains(_compareValue!), + IList list => list.Contains(_compareValue), IEnumerable enumerable => enumerable.Cast().Contains(_compareValue), _ => throw new NotSupportedException($"Invalid type `{value!.GetType().FullName}`."), }; diff --git a/src/NetEvolve.FluentValue/Constraints/EndsWithConstraint.cs b/src/NetEvolve.FluentValue/Constraints/EndsWithConstraint.cs index b1eaccf..0731341 100644 --- a/src/NetEvolve.FluentValue/Constraints/EndsWithConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/EndsWithConstraint.cs @@ -8,9 +8,9 @@ internal sealed class EndsWithConstraint : ConstraintBase private readonly object? _compareValue; private readonly StringComparison? _comparison; - public EndsWithConstraint(char compareValue) => _compareValue = compareValue; + internal EndsWithConstraint(char compareValue) => _compareValue = compareValue; - public EndsWithConstraint(string compareValue, StringComparison comparison) + internal EndsWithConstraint(string compareValue, StringComparison comparison) { _compareValue = compareValue; _comparison = comparison; diff --git a/src/NetEvolve.FluentValue/Constraints/EqualToConstraint.cs b/src/NetEvolve.FluentValue/Constraints/EqualToConstraint.cs index 0747d41..3111aca 100644 --- a/src/NetEvolve.FluentValue/Constraints/EqualToConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/EqualToConstraint.cs @@ -8,9 +8,9 @@ internal sealed class EqualToConstraint : ConstraintBase private readonly object? _compareValue; private readonly StringComparison? _comparison; - public EqualToConstraint(object? compareValue) => _compareValue = compareValue; + internal EqualToConstraint(object? compareValue) => _compareValue = compareValue; - public EqualToConstraint(string compareValue, StringComparison comparison) + internal EqualToConstraint(string compareValue, StringComparison comparison) { _compareValue = compareValue; _comparison = comparison; @@ -19,6 +19,7 @@ public EqualToConstraint(string compareValue, StringComparison comparison) public override bool IsSatisfiedBy(object? value) => value switch { + null => _compareValue is null, string stringValue when _compareValue is string compareValue => stringValue.Equals( compareValue, _comparison ?? default @@ -27,7 +28,7 @@ public override bool IsSatisfiedBy(object? value) => convertible.ToString(), _comparison ?? default ), - _ => false, + _ => value.Equals(_compareValue), }; public override void SetDescription(StringBuilder builder) => diff --git a/src/NetEvolve.FluentValue/Constraints/MatchesConstraint.cs b/src/NetEvolve.FluentValue/Constraints/MatchesConstraint.cs index 77e2448..debb58e 100644 --- a/src/NetEvolve.FluentValue/Constraints/MatchesConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/MatchesConstraint.cs @@ -12,7 +12,7 @@ internal sealed class MatchesConstraint : ConstraintBase public MatchesConstraint(string pattern, RegexOptions? options) { _pattern = pattern; - _regex = new Regex(pattern, options ?? default); + _regex = new Regex(pattern, options ?? default, TimeSpan.FromSeconds(5)); } public override bool IsSatisfiedBy(object? value) => diff --git a/src/NetEvolve.FluentValue/Constraints/ParenthesisConstraint.cs b/src/NetEvolve.FluentValue/Constraints/ParenthesisConstraint.cs index 139bb08..cb13e62 100644 --- a/src/NetEvolve.FluentValue/Constraints/ParenthesisConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/ParenthesisConstraint.cs @@ -8,7 +8,7 @@ internal sealed class ParenthesisConstraint : ConstraintBase { private readonly ConstraintBase _constraint; - public ParenthesisConstraint(IConstraint constraint) + internal ParenthesisConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); diff --git a/src/NetEvolve.FluentValue/Constraints/StartsWithConstraint.cs b/src/NetEvolve.FluentValue/Constraints/StartsWithConstraint.cs index f8d557e..08b96f9 100644 --- a/src/NetEvolve.FluentValue/Constraints/StartsWithConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/StartsWithConstraint.cs @@ -8,9 +8,9 @@ internal sealed class StartsWithConstraint : ConstraintBase private readonly object? _compareValue; private readonly StringComparison? _comparison; - public StartsWithConstraint(char compareValue) => _compareValue = compareValue; + internal StartsWithConstraint(char compareValue) => _compareValue = compareValue; - public StartsWithConstraint(string compareValue, StringComparison comparison) + internal StartsWithConstraint(string compareValue, StringComparison comparison) { _compareValue = compareValue; _comparison = comparison; diff --git a/src/NetEvolve.FluentValue/Constraints/WhiteSpaceConstraint.cs b/src/NetEvolve.FluentValue/Constraints/WhiteSpaceConstraint.cs index acc3444..e97e61d 100644 --- a/src/NetEvolve.FluentValue/Constraints/WhiteSpaceConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/WhiteSpaceConstraint.cs @@ -7,10 +7,23 @@ internal sealed class WhiteSpaceConstraint : ConstraintBase public override bool IsSatisfiedBy(object? value) => value switch { - string stringValue => string.IsNullOrWhiteSpace(stringValue), + char charValue => char.IsWhiteSpace(charValue), + string stringValue => IsWhiteSpace(stringValue), _ => false, }; public override void SetDescription(StringBuilder builder) => builder.Append(" is "); + + private static bool IsWhiteSpace(ReadOnlySpan value) + { + for (var i = 0; i < value.Length; i++) + { + if (!char.IsWhiteSpace(value[i])) + { + return false; + } + } + return true; + } } diff --git a/src/NetEvolve.FluentValue/IConstraint.cs b/src/NetEvolve.FluentValue/IConstraint.cs index 48bcbb7..2d2a641 100644 --- a/src/NetEvolve.FluentValue/IConstraint.cs +++ b/src/NetEvolve.FluentValue/IConstraint.cs @@ -41,7 +41,18 @@ public interface IConstraint "S3060:\"is\" should not be used with \"this\"", Justification = "As designed." )] - IOperator And => this is AndOperator op ? op : new AndOperator(this); + IOperator And + { + get + { + if (this is IOperator op && op is not NotOperator) + { + throw new InvalidOperationException("Cannot chain multiple operators."); + } + + return new AndOperator(this); + } + } /// /// Combines the current constraint with the given constraint using a logical OR operation. @@ -51,5 +62,37 @@ public interface IConstraint "S3060:\"is\" should not be used with \"this\"", Justification = "As designed." )] - IOperator Or => this is OrOperator op ? op : new OrOperator(this); + IOperator Or + { + get + { + if (this is IOperator op && op is not NotOperator) + { + throw new InvalidOperationException("Cannot chain multiple operators."); + } + + return new OrOperator(this); + } + } + + /// + /// Combines the current constraint with the given constraint using a logical XOR operation. + /// + [SuppressMessage( + "Blocker Code Smell", + "S3060:\"is\" should not be used with \"this\"", + Justification = "As designed." + )] + IOperator Xor + { + get + { + if (this is IOperator op && op is not NotOperator) + { + throw new InvalidOperationException("Cannot chain multiple operators."); + } + + return new XorOperator(this); + } + } } diff --git a/src/NetEvolve.FluentValue/Operators/AndOperator.cs b/src/NetEvolve.FluentValue/Operators/AndOperator.cs index ef97047..383e830 100644 --- a/src/NetEvolve.FluentValue/Operators/AndOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/AndOperator.cs @@ -1,8 +1,6 @@ namespace NetEvolve.FluentValue.Operators; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using NetEvolve.Arguments; using NetEvolve.FluentValue; @@ -10,10 +8,9 @@ internal sealed class AndOperator : OperatorBase { private readonly IConstraint _left; + private IConstraint? _right; - private readonly List _constraints = []; - - public AndOperator(IConstraint left) + internal AndOperator(IConstraint left) { Argument.ThrowIfNull(left); @@ -22,26 +19,25 @@ public AndOperator(IConstraint left) public override bool IsSatisfiedBy(object? value) { - if (_constraints.Count == 0) + if (_right is null) { throw new InvalidOperationException(); } - return _left.IsSatisfiedBy(value) && _constraints.TrueForAll(x => x.IsSatisfiedBy(value)); + return _left.IsSatisfiedBy(value) && _right.IsSatisfiedBy(value); } public override IConstraint SetConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); - var lastConstraint = _constraints.LastOrDefault(); - if (lastConstraint is NotOperator notOperator) + if (_right is NotOperator notOperator) { _ = notOperator.SetConstraint(constraint); return this; } - _constraints.Add(constraint); + _right = constraint; return this; } @@ -49,11 +45,11 @@ public override IConstraint SetConstraint(IConstraint constraint) public override void SetDescription(StringBuilder builder) { _left.SetDescription(builder); - - foreach (var constraint in _constraints) + if (_right is null) { - _ = builder.Append(" and"); - constraint.SetDescription(builder); + return; } + _ = builder.Append(" and"); + _right.SetDescription(builder); } } diff --git a/src/NetEvolve.FluentValue/Operators/NotOperator.cs b/src/NetEvolve.FluentValue/Operators/NotOperator.cs index 0ec48a2..04cf14c 100644 --- a/src/NetEvolve.FluentValue/Operators/NotOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/NotOperator.cs @@ -9,9 +9,7 @@ internal sealed class NotOperator : OperatorBase { internal IConstraint? _constraint; - public NotOperator() { } - - public NotOperator(IConstraint constraint) => _constraint = constraint; + internal NotOperator() { } public override bool IsSatisfiedBy(object? value) { diff --git a/src/NetEvolve.FluentValue/Operators/OrOperator.cs b/src/NetEvolve.FluentValue/Operators/OrOperator.cs index d6a7c24..f218b0c 100644 --- a/src/NetEvolve.FluentValue/Operators/OrOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/OrOperator.cs @@ -1,8 +1,6 @@ namespace NetEvolve.FluentValue.Operators; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using NetEvolve.Arguments; using NetEvolve.FluentValue; @@ -10,10 +8,9 @@ internal sealed class OrOperator : OperatorBase { private readonly IConstraint _left; + private IConstraint? _right; - private readonly List _constraints = []; - - public OrOperator(IConstraint left) + internal OrOperator(IConstraint left) { Argument.ThrowIfNull(left); @@ -22,26 +19,25 @@ public OrOperator(IConstraint left) public override bool IsSatisfiedBy(object? value) { - if (_constraints.Count == 0) + if (_right is null) { throw new InvalidOperationException(); } - return _left.IsSatisfiedBy(value) || _constraints.Exists(x => x.IsSatisfiedBy(value)); + return _left.IsSatisfiedBy(value) || _right.IsSatisfiedBy(value); } public override IConstraint SetConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); - var lastConstraint = _constraints.LastOrDefault(); - if (lastConstraint is NotOperator notOperator) + if (_right is NotOperator notOperator) { _ = notOperator.SetConstraint(constraint); return this; } - _constraints.Add(constraint); + _right = constraint; return this; } @@ -49,10 +45,11 @@ public override IConstraint SetConstraint(IConstraint constraint) public override void SetDescription(StringBuilder builder) { _left.SetDescription(builder); - foreach (var constraint in _constraints) + if (_right is null) { - _ = builder.Append(" or"); - constraint.SetDescription(builder); + return; } + _ = builder.Append(" or"); + _right.SetDescription(builder); } } diff --git a/src/NetEvolve.FluentValue/Operators/XorOperator.cs b/src/NetEvolve.FluentValue/Operators/XorOperator.cs new file mode 100644 index 0000000..c7d6110 --- /dev/null +++ b/src/NetEvolve.FluentValue/Operators/XorOperator.cs @@ -0,0 +1,55 @@ +namespace NetEvolve.FluentValue.Operators; + +using System; +using System.Text; +using NetEvolve.Arguments; +using NetEvolve.FluentValue; + +internal sealed class XorOperator : OperatorBase +{ + private readonly IConstraint _left; + private IConstraint? _right; + + internal XorOperator(IConstraint left) + { + Argument.ThrowIfNull(left); + + _left = left; + } + + public override bool IsSatisfiedBy(object? value) + { + if (_right is null) + { + throw new InvalidOperationException(); + } + + return _left.IsSatisfiedBy(value) ^ _right.IsSatisfiedBy(value); + } + + public override IConstraint SetConstraint(IConstraint constraint) + { + Argument.ThrowIfNull(constraint); + + if (_right is NotOperator notOperator) + { + _ = notOperator.SetConstraint(constraint); + return this; + } + + _right = constraint; + + return this; + } + + public override void SetDescription(StringBuilder builder) + { + _left.SetDescription(builder); + if (_right is null) + { + return; + } + _ = builder.Append(" xor"); + _right.SetDescription(builder); + } +} diff --git a/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs b/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs index 6afaa24..9e3fe7b 100644 --- a/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs +++ b/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs @@ -13,6 +13,20 @@ public class ConstraintTests public void Value_NotNot_NoFurtherConstraint_ThrowsInvalidOperationException() => _ = Assert.Throws(() => Value.Not.Not); + [Fact] + public void Value_MultipleOperators_ThrowsInvalidOperationException() => + Assert.Multiple( + () => _ = Assert.Throws(() => Value.Null.And.And), + () => _ = Assert.Throws(() => Value.Null.And.Or), + () => _ = Assert.Throws(() => Value.Null.And.Xor), + () => _ = Assert.Throws(() => Value.Null.Or.And), + () => _ = Assert.Throws(() => Value.Null.Or.Or), + () => _ = Assert.Throws(() => Value.Null.Or.Xor), + () => _ = Assert.Throws(() => Value.Null.Xor.And), + () => _ = Assert.Throws(() => Value.Null.Xor.Or), + () => _ = Assert.Throws(() => Value.Null.Xor.Xor) + ); + [Fact] public void Value_Contains_Object_ThrowsNotSupportedException() => _ = Assert.Throws( @@ -25,7 +39,15 @@ public void Value_InvalidConstraint_ThrowsInvalidOperationException(IConstraint _ = Assert.Throws(() => constraint.IsSatisfiedBy(true)); public static TheoryData InvalidConstraintData => - [Value.Not, Value.Null.And, Value.Not.And, Value.Null.Or]; + [ + Value.Not, + Value.Null.And, + Value.Null.Or, + Value.Null.Xor, + Value.Not.Null.And, + Value.Not.Null.Or, + Value.Not.Null.Xor, + ]; [Theory] [MemberData(nameof(ConstraintValueData))] @@ -72,6 +94,7 @@ public void Value_Theory_Expected(bool expected, IConstraint constraint, object? // .Default { false, Value.Default, 1 }, { false, Value.Default, null }, + { false, Value.Default, (int?)3 }, { true, Value.Default, 0 }, // .Empty { false, Value.Empty, "Hello World!" }, @@ -86,18 +109,22 @@ public void Value_Theory_Expected(bool expected, IConstraint constraint, object? { true, Value.Empty, new List() }, { false, Value.Empty, Enumerable.Range(10, 10) }, { true, Value.Empty, Enumerable.Empty() }, + { false, Value.Empty, null }, // .EndsWith { false, Value.EndsWith("Welt!"), "Hello World!" }, { true, Value.EndsWith("World!"), "Hello World!" }, { false, Value.EndsWith('?'), "Hello World!" }, { true, Value.EndsWith('!'), "Hello World!" }, { true, Value.EndsWith("world!", OrdinalIgnoreCase), "Hello World!" }, + { false, Value.EndsWith("123"), 123 }, // .EqualTo { false, Value.EqualTo("World!"), "Hello World!" }, { true, Value.EqualTo("Hello World!"), "Hello World!" }, { false, Value.EqualTo('H'), "Hello World!" }, { true, Value.EqualTo(123456), "123456" }, { true, Value.EqualTo("hello world!", OrdinalIgnoreCase), "Hello World!" }, + { true, Value.EqualTo(123), 123 }, + { true, Value.EqualTo(null), null }, // .Matches { false, Value.Matches(@"\d+"), null }, { true, Value.Matches(@"\d+"), 123456 }, @@ -111,14 +138,16 @@ public void Value_Theory_Expected(bool expected, IConstraint constraint, object? { false, Value.StartsWith('?'), "Hello World!" }, { true, Value.StartsWith('H'), "Hello World!" }, { true, Value.StartsWith("hello", OrdinalIgnoreCase), "Hello World!" }, + { false, Value.StartsWith("123"), 123 }, // .WhiteSpace { false, Value.WhiteSpace, "Hello World!" }, + { false, Value.WhiteSpace, null }, { true, Value.WhiteSpace, " " }, { true, Value.WhiteSpace, "\t" }, { true, Value.WhiteSpace, "\n" }, - { true, Value.WhiteSpace, "\r" }, - { true, Value.WhiteSpace, "\v" }, - { true, Value.WhiteSpace, "\f" }, + { true, Value.WhiteSpace, '\t' }, + { true, Value.WhiteSpace, '\n' }, + { false, Value.WhiteSpace, 1 }, // .And Operators { false, @@ -134,6 +163,19 @@ public void Value_Theory_Expected(bool expected, IConstraint constraint, object? { false, Value.Null.Or.Empty, "Hello World!" }, { true, Value.Null.Or.Empty, null }, { true, Value.Null.Or.Empty, string.Empty }, + // .Xor Operators + { false, Value.Null.Xor.Empty, "Hello World!" }, + { true, Value.Null.Xor.Empty, string.Empty }, + { + false, + Value.Contains("Hello", OrdinalIgnoreCase).Xor.Contains("World!", Ordinal), + "Hello World!" + }, + { + true, + Value.Contains("Hello", OrdinalIgnoreCase).Xor.Not.Contains("World!", Ordinal), + "Hello World!" + }, // .Not Operators { false, Value.Not.EqualTo(2), "2" }, { false, Value.Not.EqualTo("Hello World!"), "Hello World!" }, @@ -154,6 +196,9 @@ public void Value_Theory_Expected(bool expected, IConstraint constraint, object? { true, Value.Not.Parenthesis(Value.Null.Or.Empty), "Hello World!" }, { false, Value.Not.Parenthesis(Value.Null.Or.Empty), null }, { false, Value.Not.Parenthesis(Value.Null.Or.Empty), string.Empty }, + { false, Value.Parenthesis(Value.Null.Or.Empty), "Hello World!" }, + { true, Value.Parenthesis(Value.Null.Or.Empty), null }, + { true, Value.Parenthesis(Value.Null.Or.Empty), string.Empty }, // .And.Not { true, Value.Not.Null.And.Not.Empty, "Hello World!" }, { false, Value.Not.Null.And.Not.Empty, null },