Repository: bartoszlenar/Validot Branch: main Commit: 8497d506e6ce Files: 389 Total size: 2.7 MB Directory structure: gitextract_6qgujrz3/ ├── .editorconfig ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── README.md │ ├── pull_request_template.md │ └── workflows/ │ ├── CI.yml │ └── Init-WorkflowVariables.ps1 ├── .gitignore ├── .nuke/ │ ├── build.schema.json │ └── parameters.json ├── LICENSE ├── Validot.sln ├── build/ │ ├── .editorconfig │ ├── Build.cs │ ├── Configuration.cs │ ├── _build.csproj │ └── _build.csproj.DotSettings ├── build.cmd ├── build.ps1 ├── build.sh ├── docs/ │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── DOCUMENTATION.md │ └── articles/ │ ├── crafting-model-specifications-using-validot.md │ └── validots-performance-explained.md ├── nuget.config ├── src/ │ └── Validot/ │ ├── CodeHelper.cs │ ├── Errors/ │ │ ├── Args/ │ │ │ ├── ArgHelper.cs │ │ │ ├── ArgPlaceholder.cs │ │ │ ├── EnumArg.cs │ │ │ ├── EnumArgFactory.cs │ │ │ ├── GuidArg.cs │ │ │ ├── GuidArgFactory.cs │ │ │ ├── IArg.cs │ │ │ ├── NameArg.cs │ │ │ ├── NumberArg.cs │ │ │ ├── NumberArgFactory.cs │ │ │ ├── TextArg.cs │ │ │ ├── TextArgFactory.cs │ │ │ ├── TimeArg.cs │ │ │ ├── TimeArgFactory.cs │ │ │ ├── TranslationArg.cs │ │ │ ├── TypeArg.cs │ │ │ └── TypeArgFactory.cs │ │ ├── CacheIntegrityException.cs │ │ ├── Error.cs │ │ ├── IError.cs │ │ ├── IMessageService.cs │ │ ├── MessageCache.cs │ │ ├── MessageService.cs │ │ ├── ReferenceLoopError.cs │ │ └── Translator/ │ │ ├── MessageTranslator.cs │ │ └── TranslationResult.cs │ ├── Factory/ │ │ ├── HolderInfo.cs │ │ ├── ISettingsHolder.cs │ │ ├── ISpecificationHolder.cs │ │ └── ValidatorFactory.cs │ ├── IValidator.cs │ ├── PathHelper.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Results/ │ │ ├── IValidationResult.cs │ │ └── ValidationResult.cs │ ├── Rules/ │ │ ├── BoolRules.cs │ │ ├── CharRules.cs │ │ ├── Collections/ │ │ │ ├── ArrayRules.cs │ │ │ ├── BaseCollectionRules.cs │ │ │ ├── IEnumerableRules.cs │ │ │ ├── IListRules.cs │ │ │ ├── IReadOnlyCollectionRules.cs │ │ │ ├── IReadOnlyListRules.cs │ │ │ └── ListRules.cs │ │ ├── GuidRules.cs │ │ ├── Numbers/ │ │ │ ├── ByteRules.cs │ │ │ ├── CharNumbersRules.cs │ │ │ ├── DecimalRules.cs │ │ │ ├── DoubleRules.cs │ │ │ ├── FloatRules.cs │ │ │ ├── IntRules.cs │ │ │ ├── LongRules.cs │ │ │ ├── SByteRules.cs │ │ │ ├── ShortRules.cs │ │ │ ├── UIntRules.cs │ │ │ ├── ULongRules.cs │ │ │ └── UShortRules.cs │ │ ├── Text/ │ │ │ ├── EmailRules.cs │ │ │ ├── EmailValidationMode.cs │ │ │ └── StringRules.cs │ │ ├── TimeSpanRules.cs │ │ └── Times/ │ │ ├── DateTimeFormats.cs │ │ ├── DateTimeOffsetRules.cs │ │ ├── DateTimeRules.cs │ │ ├── TimeComparer.cs │ │ └── TimeComparison.cs │ ├── Settings/ │ │ ├── IValidatorSettings.cs │ │ ├── ValidatorSettings.cs │ │ └── ValidatorSettingsExtensions.cs │ ├── Specification/ │ │ ├── AndExtension.cs │ │ ├── AsCollectionExtension.cs │ │ ├── AsConvertedExtension.cs │ │ ├── AsDictionaryExtension.cs │ │ ├── AsDictionaryWithStringKeyExtension.cs │ │ ├── AsModelExtension.cs │ │ ├── AsNullableExtension.cs │ │ ├── AsTypeExtension.cs │ │ ├── Commands/ │ │ │ ├── AsCollectionCommand.cs │ │ │ ├── AsConvertedCommand.cs │ │ │ ├── AsDictionaryCommand.cs │ │ │ ├── AsModelCommand.cs │ │ │ ├── AsNullableCommand.cs │ │ │ ├── AsTypeCommand.cs │ │ │ ├── ForbiddenCommand.cs │ │ │ ├── ICommand.cs │ │ │ ├── IScopeCommand.cs │ │ │ ├── MemberCommand.cs │ │ │ ├── OptionalCommand.cs │ │ │ ├── RequiredCommand.cs │ │ │ ├── RuleCommand.cs │ │ │ ├── WithCodeCommand.cs │ │ │ ├── WithConditionCommand.cs │ │ │ ├── WithExtraCodeCommand.cs │ │ │ ├── WithExtraMessageCommand.cs │ │ │ ├── WithMessageCommand.cs │ │ │ └── WithPathCommand.cs │ │ ├── ForbiddenExtension.cs │ │ ├── MemberExtension.cs │ │ ├── OptionalExtension.cs │ │ ├── RequiredExtension.cs │ │ ├── RuleExtension.cs │ │ ├── Specification.cs │ │ ├── SpecificationApi.cs │ │ ├── WithCodeExtension.cs │ │ ├── WithConditionExtension.cs │ │ ├── WithExtraCodeExtension.cs │ │ ├── WithExtraMessageExtension.cs │ │ ├── WithMessageExtension.cs │ │ └── WithPathExtension.cs │ ├── Testing/ │ │ ├── TestFailedException.cs │ │ ├── TestResult.cs │ │ ├── Tester.cs │ │ └── ToStringContentType.cs │ ├── ThrowHelper.cs │ ├── Translations/ │ │ ├── Chinese/ │ │ │ ├── ChineseTranslation.cs │ │ │ └── ChineseTranslationsExtensions.cs │ │ ├── English/ │ │ │ ├── EnglishTranslation.cs │ │ │ └── EnglishTranslationsExtensions.cs │ │ ├── German/ │ │ │ ├── GermanTranslation.cs │ │ │ └── GermanTranslationsExtensions.cs │ │ ├── ITranslationCompiler.cs │ │ ├── MessageKey.cs │ │ ├── Polish/ │ │ │ ├── PolishTranslation.cs │ │ │ └── PolishTranslationsExtensions.cs │ │ ├── Portuguese/ │ │ │ ├── PortugueseTranslation.cs │ │ │ └── PortugueseTranslationsExtensions.cs │ │ ├── Russian/ │ │ │ ├── RussianTranslation.cs │ │ │ └── RussianTranslationsExtensions.cs │ │ ├── Spanish/ │ │ │ ├── SpanishTranslation.cs │ │ │ └── SpanishTranslationsExtensions.cs │ │ ├── TranslationCompiler.cs │ │ └── _Template/ │ │ ├── _TemplateTranslation.cs.txt │ │ └── _TemplateTranslationsExtensions.cs.txt │ ├── TypeStringifier.cs │ ├── Validation/ │ │ ├── DiscoveryContext.cs │ │ ├── ErrorFlag.cs │ │ ├── IDiscoveryContext.cs │ │ ├── IDiscoveryContextActions.cs │ │ ├── IErrorsHolder.cs │ │ ├── IValidationContext.cs │ │ ├── IsValidValidationContext.cs │ │ ├── Scheme/ │ │ │ ├── IModelScheme.cs │ │ │ ├── ModelScheme.cs │ │ │ └── ModelSchemeFactory.cs │ │ ├── Scopes/ │ │ │ ├── Builders/ │ │ │ │ ├── CommandScopeBuilder.cs │ │ │ │ ├── ErrorBuilder.cs │ │ │ │ ├── ErrorMode.cs │ │ │ │ ├── ICommandScopeBuilder.cs │ │ │ │ ├── IScopeBuilderContext.cs │ │ │ │ ├── RuleCommandScopeBuilder.cs │ │ │ │ ├── ScopeBuilder.cs │ │ │ │ └── ScopeBuilderContext.cs │ │ │ ├── CollectionCommandScope.cs │ │ │ ├── CommandScope.cs │ │ │ ├── ConvertedCommandScope.cs │ │ │ ├── DictionaryCommandScope.cs │ │ │ ├── ICommandScope.cs │ │ │ ├── IDiscoverable.cs │ │ │ ├── IScope.cs │ │ │ ├── ISpecificationScope.cs │ │ │ ├── IValidatable.cs │ │ │ ├── MemberCommandScope.cs │ │ │ ├── ModelCommandScope.cs │ │ │ ├── NullableCommandScope.cs │ │ │ ├── Presence.cs │ │ │ ├── RuleCommandScope.cs │ │ │ ├── SpecificationScope.cs │ │ │ └── TypeCommandScope.cs │ │ ├── Stacks/ │ │ │ ├── PathStack.cs │ │ │ ├── ReferenceLoopException.cs │ │ │ ├── ReferenceLoopProtectionSettings.cs │ │ │ └── ReferencesStack.cs │ │ └── ValidationContext.cs │ ├── Validator.cs │ ├── Validot.csproj │ └── ValidotException.cs └── tests/ ├── AssemblyWithHolders/ │ ├── AssemblyWithHolders.csproj │ ├── AssemblyWithHoldersHook.cs │ ├── HolderOfDecimalSpecification.cs │ ├── HolderOfIntSpecificationAndSettings.cs │ ├── HolderOfMultipleSpecifications.cs │ ├── HolderOfMultipleSpecificationsAndSettings.cs │ ├── HolderOfStringSpecification.cs │ ├── HolderOfStringSpecificationAndSettings.cs │ ├── NestedHolders.cs │ ├── PrivateSpecificationAndSettingsHolder.cs │ ├── PrivateSpecificationHolder.cs │ └── Properties/ │ └── AssemblyInfo.cs ├── Validot.Benchmarks/ │ ├── .editorconfig │ ├── Comparisons/ │ │ ├── ComparisonDataSet.cs │ │ ├── EngineOnlyBenchmark.cs │ │ ├── InitializationBenchmark.cs │ │ ├── ReportingBenchmark.cs │ │ ├── ToStringBenchmark.cs │ │ └── ValidationBenchmark.cs │ ├── EmailModesBenchmark.cs │ ├── Program.cs │ └── Validot.Benchmarks.csproj ├── Validot.MemoryLeak/ │ ├── .editorconfig │ ├── Program.cs │ ├── StreamDataSet.cs │ ├── Validot.MemoryLeak.csproj │ └── Validot.MemoryLeak.dockerfile ├── Validot.Tests.Functional/ │ ├── .editorconfig │ ├── ConcurrencyFuncTests.cs │ ├── Documentation/ │ │ ├── CustomRulesFuncTests.cs │ │ ├── ErrorOutputFuncTests.cs │ │ ├── FactoryFuncTests.cs │ │ ├── FluentApiFuncTests.cs │ │ ├── MessageArgumentsFuncTests.cs │ │ ├── Models/ │ │ │ ├── A.cs │ │ │ ├── AuthorModel.cs │ │ │ ├── B.cs │ │ │ ├── BookModel.cs │ │ │ ├── Language.cs │ │ │ └── PublisherModel.cs │ │ ├── ParameterCommandsFuncTests.cs │ │ ├── PresenceCommandsFuncTests.cs │ │ ├── ReferenceLoopFuncTests.cs │ │ ├── ResultFuncTests.cs │ │ ├── ScopeCommandsFuncTests.cs │ │ ├── SettingsFuncTests.cs │ │ ├── SpecificationFuncTests.cs │ │ ├── TranslationsFuncTests.cs │ │ └── ValidatorFuncTests.cs │ ├── Readme/ │ │ ├── FeaturesFuncTests.cs │ │ └── QuickStartFuncTests.cs │ └── Validot.Tests.Functional.csproj └── Validot.Tests.Unit/ ├── .editorconfig ├── CodeHelperTests.cs ├── ErrorContentApiHelper.cs ├── ErrorSetupApiHelper.cs ├── Errors/ │ ├── Args/ │ │ ├── ArgHelperTests.cs │ │ ├── EnumArgTests.cs │ │ ├── GuidArgTests.cs │ │ ├── NameArgTests.cs │ │ ├── NumberArgTests.cs │ │ ├── TextArgTests.cs │ │ ├── TimeArgTests.cs │ │ ├── TranslationArgTests.cs │ │ └── TypeArgTests.cs │ ├── MessageCacheTests.cs │ ├── MessageServiceTests.cs │ ├── ReferenceLoopErrorTests.cs │ └── Translator/ │ └── MessageTranslatorTests.cs ├── Factory/ │ ├── HolderInfoTests.cs │ └── ValidatorFactoryTests.cs ├── GuardTests.cs ├── PathHelperTests.cs ├── PathTestData.cs ├── Results/ │ └── ValidationResultTests.cs ├── Rules/ │ ├── BoolRulesTests.cs │ ├── CharRulesTests.cs │ ├── Collections/ │ │ ├── ArrayRulesTests.cs │ │ ├── BaseCollectionRulesTests.cs │ │ ├── CollectionsTestData.cs │ │ ├── IEnumerableRulesTests.cs │ │ ├── IListRulesTests.cs │ │ ├── IReadOnlyCollectionRulesTests.cs │ │ ├── IReadOnlyListRulesTests.cs │ │ └── ListRulesTests.cs │ ├── GuidRulesTests.cs │ ├── Numbers/ │ │ ├── ByteRulesTests.cs │ │ ├── CharNumbersRulesTests.cs │ │ ├── DecimalRulesTests.cs │ │ ├── DoubleRulesTests.cs │ │ ├── FloatRulesTests.cs │ │ ├── IntRulesTests.cs │ │ ├── LongRulesTests.cs │ │ ├── NumbersTestData.cs │ │ ├── SByteRulesTests.cs │ │ ├── ShortRulesTests.cs │ │ ├── UIntRulesTests.cs │ │ ├── ULongRulesTests.cs │ │ └── UShortRulesTests.cs │ ├── RulesHelper.cs │ ├── Text/ │ │ ├── CharRulesTests.cs │ │ ├── EmailRulesTests.cs │ │ └── StringRulesTests.cs │ ├── TimeSpanRulesTests.cs │ └── Times/ │ ├── DateTimeOffsetRulesTests.cs │ ├── DateTimeRulesTests.cs │ └── TimesTestData.cs ├── Settings/ │ ├── ValidatorSettingsExtensionsTests.cs │ ├── ValidatorSettingsTestHelpers.cs │ └── ValidatorSettingsTests.cs ├── Specification/ │ ├── AndExtensionTests.cs │ ├── ApiTester.cs │ ├── AsCollectionExtensionTests.cs │ ├── AsConvertedExtensionTests.cs │ ├── AsDictionaryExtensionTests.cs │ ├── AsDictionaryWithKeyStringExtensionTests.cs │ ├── AsModelExtensionTests.cs │ ├── AsNullableExtensionTests.cs │ ├── AsTypeExtensionTests.cs │ ├── Commands/ │ │ ├── AsCollectionCommandTests.cs │ │ ├── AsConvertedCommandTests.cs │ │ ├── AsDictionaryCommandTests.cs │ │ ├── AsModelCommandTests.cs │ │ ├── AsNullableCommandTests.cs │ │ ├── AsTypeCommandTests.cs │ │ ├── MemberCommandTests.cs │ │ └── RuleCommandTests.cs │ ├── ForbiddenExtensionTests.cs │ ├── MemberExtensionTests.cs │ ├── OptionalExtensionTests.cs │ ├── RequiredExtensionTests.cs │ ├── RuleExtensionTests.cs │ ├── WithCodeExtensionTests.cs │ ├── WithConditionExtensionTests.cs │ ├── WithExtraCodeExtensionTests.cs │ ├── WithExtraMessageExtensionTests.cs │ ├── WithMessageExtensionTests.cs │ └── WithPathExtensionTests.cs ├── TempTests.cs ├── Testing/ │ └── TesterTests.cs ├── Translations/ │ ├── Chinese/ │ │ └── ChineseTranslationsExtensionsTests.cs │ ├── English/ │ │ └── EnglishTranslationsExtensionsTests.cs │ ├── German/ │ │ └── GermanTranslationsExtensionsTests.cs │ ├── MessageKeyTests.cs │ ├── Polish/ │ │ └── PolishTranslationsExtensionsTests.cs │ ├── Portuguese/ │ │ └── PortugueseTranslationsExtensionsTests.cs │ ├── Russian/ │ │ └── RussianTranslationsExtensionsTests.cs │ ├── Spanish/ │ │ └── SpanishTranslationsExtensionsTests.cs │ ├── TranslationCompilerTests.cs │ ├── TranslationTestHelpers.cs │ └── _Template/ │ └── _TemplateTranslationsExtensionsTests.cs.txt ├── TypeStringifierTests.cs ├── Validation/ │ ├── DiscoveryContextTests.cs │ ├── ErrorFlagTests.cs │ ├── IsValidValidationContextTests.cs │ ├── Scheme/ │ │ ├── ModelSchemeFactoryTests.cs │ │ └── ModelSchemeTests.cs │ ├── Scopes/ │ │ ├── Builders/ │ │ │ ├── CommandScopeBuilderTests.cs │ │ │ ├── ErrorBuilderTestData.cs │ │ │ ├── ErrorBuilderTests.cs │ │ │ ├── ErrorTestsHelpers.cs │ │ │ ├── RuleCommandScopeBuilderTests.cs │ │ │ ├── ScopeBuilderContextTests.cs │ │ │ └── ScopeBuilderTests.cs │ │ ├── CollectionCommandScopeTests.cs │ │ ├── CommandScopeTestHelper.cs │ │ ├── ConvertedCommandScopeTests.cs │ │ ├── DictionaryCommandScopeTests.cs │ │ ├── MemberCommandScopeTests.cs │ │ ├── ModelCommandScopeTests.cs │ │ ├── NullableCommandScopeTests.cs │ │ ├── RuleCommandScopeTests.cs │ │ └── SpecificationScopeTests.cs │ ├── Stack/ │ │ ├── PathStackTests.cs │ │ ├── ReferenceLoopExceptionTests.cs │ │ ├── ReferenceLoopProtectionSettingsTests.cs │ │ └── ReferencesStackTests.cs │ ├── TraversingTestCases.cs │ └── ValidationContextTests.cs ├── ValidationTestData.cs ├── ValidationTestHelpers.cs ├── ValidatorTests.cs ├── Validot.Tests.Unit.csproj └── ValidotExceptionTests.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true # C# files [*.cs] #### Core EditorConfig Options #### # Indentation and spacing indent_size = 4 indent_style = space tab_width = 4 # New line preferences end_of_line = crlf insert_final_newline = false #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = false file_header_template = unset # this. and Me. preferences dotnet_style_qualification_for_event = false:silent dotnet_style_qualification_for_field = false:silent dotnet_style_qualification_for_method = false:silent dotnet_style_qualification_for_property = false:silent # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:silent dotnet_style_predefined_type_for_member_access = true:silent # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent # Expression-level preferences dotnet_style_coalesce_expression = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_object_initializer = true:suggestion dotnet_style_operator_placement_when_wrapping = beginning_of_line dotnet_style_prefer_auto_properties = true:silent dotnet_style_prefer_compound_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_assignment = false:silent dotnet_style_prefer_conditional_expression_over_return = false:silent dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion dotnet_style_prefer_simplified_boolean_expressions = true:suggestion dotnet_style_prefer_simplified_interpolation = true:suggestion # Field preferences dotnet_style_readonly_field = true:suggestion # Parameter preferences dotnet_code_quality_unused_parameters = all:suggestion # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none #### C# Coding Conventions #### # var preferences csharp_style_var_elsewhere = false:silent csharp_style_var_for_built_in_types = false:silent csharp_style_var_when_type_is_apparent = false:silent # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent csharp_style_expression_bodied_lambdas = true:silent csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = when_on_single_line:silent # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_prefer_not_pattern = true:suggestion csharp_style_prefer_pattern_matching = true:silent csharp_style_prefer_switch_expression = true:suggestion # Null-checking preferences csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences csharp_prefer_static_local_function = true:suggestion csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent # Code-block preferences csharp_prefer_braces = true:silent csharp_prefer_simple_using_statement = true:suggestion # Expression-level preferences csharp_prefer_simple_default_expression = true:suggestion csharp_style_deconstructed_variable_declaration = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_style_pattern_local_over_anonymous_function = true:suggestion csharp_style_prefer_index_operator = true:suggestion csharp_style_prefer_range_operator = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_unused_value_assignment_preference = discard_variable:suggestion csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' directive preferences csharp_using_directive_placement = inside_namespace:silent #### C# Formatting Rules #### # New line preferences csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = true csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # Wrapping preferences csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true #### Naming styles #### # Naming rules dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.pascal_case.required_prefix = dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case # ReSharper/Rider style resharper_place_simple_initializer_on_single_line=false resharper_wrap_object_and_collection_initializer_style=wrap_if_long resharper_blank_lines_between_using_groups=1 resharper_csharp_wrap_lines=false resharper_qualified_using_at_nested_scope=true resharper_blank_lines_after_control_transfer_statements=1 resharper_blank_lines_after_multiline_statements=1 resharper_blank_lines_around_block_case_section=1 resharper_blank_lines_around_multiline_case_section=1 resharper_blank_lines_around_single_line_auto_property=1 resharper_blank_lines_around_single_line_local_method=1 resharper_blank_lines_around_single_line_property=1 resharper_blank_lines_before_block_statements=1 resharper_blank_lines_before_case=1 resharper_blank_lines_before_control_transfer_statements=1 resharper_blank_lines_before_multiline_statements=1 resharper_blank_lines_inside_type=0 resharper_csharp_blank_lines_around_single_line_field=1 resharper_csharp_blank_lines_around_single_line_invocable=1 resharper_csharp_keep_blank_lines_in_code=1 resharper_csharp_keep_blank_lines_in_declarations=1 resharper_space_within_single_line_array_initializer_braces=true # Resharper/Rider inspections resharper_check_namespace_highlighting = suggestion resharper_unused_type_parameter_highlighting = suggestion resharper_unused_auto_property_accessor_local_highlighting = suggestion resharper_redundant_extends_list_entry_highlighting = suggestion # Severity settings dotnet_analyzer_diagnostic.severity = error dotnet_diagnostic.IDE0004.severity = none dotnet_diagnostic.IDE0008.severity = none dotnet_diagnostic.IDE0046.severity = none dotnet_diagnostic.IDE0057.severity = suggestion dotnet_diagnostic.IDE0090.severity = none dotnet_diagnostic.IDE0130.severity = none dotnet_diagnostic.CA1040.severity = suggestion dotnet_diagnostic.CA1032.severity = suggestion dotnet_diagnostic.CA1034.severity = suggestion dotnet_diagnostic.CA1303.severity = none dotnet_diagnostic.CA1307.severity = suggestion dotnet_diagnostic.CA1308.severity = suggestion dotnet_diagnostic.CA1716.severity = suggestion dotnet_diagnostic.CA1721.severity = suggestion dotnet_diagnostic.CA1724.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.SA0001.severity = none dotnet_diagnostic.SA1009.severity = none dotnet_diagnostic.SA1101.severity = none dotnet_diagnostic.SA1111.severity = none dotnet_diagnostic.SA1201.severity = suggestion dotnet_diagnostic.SA1309.severity = none dotnet_diagnostic.SA1402.severity = suggestion dotnet_diagnostic.SA1403.severity = suggestion dotnet_diagnostic.SA1413.severity = none dotnet_diagnostic.SA1600.severity = none dotnet_diagnostic.SA1601.severity = none dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SYSLIB1046.severity = suggestion ================================================ FILE: .github/CODEOWNERS ================================================ * bartosz@lenar.dev ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help improve the the Validot project title: "" labels: bug assignees: bartoszlenar --- ## Bug description * Please provide the bug description in points. * What is the current behavior? * What is affected by the bug? ## Bug in action ``` csharp // Most of the Validot demos can be wrapped within short snippets of code ``` ``` csharp // If the code is spread across multiple files, please contain each one in a separate block ``` Steps to reproduce: 1. Additionally... 1. or alternatively (then please remove this list)... 1. please include all steps to reproduce the bug ## Solution * Please provide the suggested solution in points. * What is the expected behavior? * What could be a possible fix/solution? ## Environment * Validot version: * dotnet version: * OS: ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Validot on StackOverflow url: https://stackoverflow.com/questions/tagged/validot about: 'Please ask and answer questions using the tag #validot' ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for the Validot project title: "" labels: enhancement assignees: bartoszlenar --- ## Feature description * Please provide the feature description in points. * What is the problem you're trying to solve? * What is in-scope and what is out-of-scope? ## Feature in action ``` csharp // Most of the Validot demos can be wrapped within short snippets of code ``` ``` csharp // If the code is spread across multiple files, please contain each one in a separate block ``` ## Feature details * Please provide the feature details in points. * Technical implementation details? * Caveats and considerations for the future? * Is this might be a breaking change? ## Discussion * Questions to the maintainers or users? * Pools? ================================================ FILE: .github/README.md ================================================



Validot

Validot is a performance-first, compact library for advanced model validation. Using a simple declarative fluent interface, it efficiently handles classes, structs, nested members, collections, nullables, plus any relation or combination of them. It also supports translations, custom logic extensions with tests, and DI containers.


Quickstart | Features | Project info | Documentation | Changelog

🔥⚔️ Validot vs FluentValidation ⚔️🔥

📜 Article: Validot's performance explained
📜 Article: Crafting model specifications using Validot

Built with 🤘🏻by Bartosz Lenar


## Announcement! Validot is archived! Validot has been my public pet project since 2020, a proof-of-concept turned into a standalone, fully-featured NuGet library. Its core focus is on performance and a low-allocation approach. On release day, Validot was 2.5 times faster while consuming 8 times less memory than the industry's gold standard: FluentValidation! I pushed dotnet memory performance and optimizations to their limits. And I'm proud of my work. Given my daily responsibilities and other coding projects, I have to confess that it seems improbable I'll have time to keep working on Validot. I appreciate all the contributors, and especially, I extend my gratitude to Jeremy Skinner for his work on FluentValidation. I genuinely believe I played a role in the open source community by motivating Jeremy to enhance FluentValidation's performance. ## Quickstart Add the Validot nuget package to your project using dotnet CLI: ``` dotnet add package Validot ``` All the features are accessible after referencing single namespace: ``` csharp using Validot; ``` And you're good to go! At first, create a specification for your model with the fluent api. ``` csharp Specification specification = _ => _ .Member(m => m.Email, m => m .Email() .WithExtraCode("ERR_EMAIL") .And() .MaxLength(100) ) .Member(m => m.Name, m => m .Optional() .And() .LengthBetween(8, 100) .And() .Rule(name => name.All(char.IsLetterOrDigit)) .WithMessage("Must contain only letter or digits") ) .And() .Rule(m => m.Age >= 18 || m.Name != null) .WithPath("Name") .WithMessage("Required for underaged user") .WithExtraCode("ERR_NAME"); ``` The next step is to create a [validator](../docs/DOCUMENTATION.md#validator). As its name stands - it validates objects according to the [specification](../docs/DOCUMENTATION.md#specification). It's also thread-safe so you can seamlessly register it as a singleton in your DI container. ``` csharp var validator = Validator.Factory.Create(specification); ``` Validate the object! ``` csharp var model = new UserModel(email: "inv@lidv@lue", age: 14); var result = validator.Validate(model); ``` The [result](../docs/DOCUMENTATION.md#result) object contains all information about the [errors](../docs/DOCUMENTATION.md#error-output). Without retriggering the validation process, you can extract the desired form of an output. ``` csharp result.AnyErrors; // bool flag: // true result.MessageMap["Email"] // collection of messages for "Email": // [ "Must be a valid email address" ] result.Codes; // collection of all the codes from the model: // [ "ERR_EMAIL", "ERR_NAME" ] result.ToString(); // compact printing of codes and messages: // ERR_EMAIL, ERR_NAME // // Email: Must be a valid email address // Name: Required for underaged user ``` * [See this example's real code](../tests/Validot.Tests.Functional/Readme/QuickStartFuncTests.cs) ## Features ### Advanced fluent API, inline No more obligatory if-ology around input models or separate classes wrapping just validation logic. Write [specifications](../docs/DOCUMENTATION.md#specification) inline with simple, human-readable [fluent API](../docs/DOCUMENTATION.md#fluent0api). Native support for properties and fields, structs and classes, [nullables](../docs/DOCUMENTATION.md#asnullable), [collections](../docs/DOCUMENTATION.md#ascollection), [nested members](../docs/DOCUMENTATION.md#member), and possible combinations. ``` csharp Specification nameSpecification = s => s .LengthBetween(5, 50) .SingleLine() .Rule(name => name.All(char.IsLetterOrDigit)); Specification emailSpecification = s => s .Email() .And() .Rule(email => email.All(char.IsLower)) .WithMessage("Must contain only lower case characters"); Specification userSpecification = s => s .Member(m => m.Name, nameSpecification) .WithMessage("Must comply with name rules") .And() .Member(m => m.PrimaryEmail, emailSpecification) .And() .Member(m => m.AlternativeEmails, m => m .Optional() .And() .MaxCollectionSize(3) .WithMessage("Must not contain more than 3 addresses") .And() .AsCollection(emailSpecification) ) .And() .Rule(user => { return user.PrimaryEmail is null || user.AlternativeEmails?.Contains(user.PrimaryEmail) == false; }) .WithMessage("Alternative emails must not contain the primary email address"); ``` * [Blog post about constructing specifications in Validot](https://lenar.dev/posts/crafting-model-specifications-using-validot) * [Guide through Validot's fluent API](../docs/DOCUMENTATION.md#fluent-api) * [If you prefer the approach of having a separate class for just validation logic, it's also fully supported](../docs/DOCUMENTATION.md#specification-holder) ### Validators Compact, highly optimized, and thread-safe objects to handle the validation. ``` csharp Specification bookSpecification = s => s .Optional() .Member(m => m.AuthorEmail, m => m.Optional().Email()) .Member(m => m.Title, m => m.NotEmpty().LengthBetween(1, 100)) .Member(m => m.Price, m => m.NonNegative()); var bookValidator = Validator.Factory.Create(bookSpecification); services.AddSingleton>(bookValidator); ``` ``` csharp var bookModel = new BookModel() { AuthorEmail = "inv@lid_em@il", Price = 10 }; bookValidator.IsValid(bookModel); // false bookValidator.Validate(bookModel).ToString(); // AuthorEmail: Must be a valid email address // Title: Required bookValidator.Validate(bookModel, failFast: true).ToString(); // AuthorEmail: Must be a valid email address bookValidator.Template.ToString(); // Template contains all of the possible errors: // AuthorEmail: Must be a valid email address // Title: Required // Title: Must not be empty // Title: Must be between 1 and 100 characters in length // Price: Must not be negative ``` * [What Validator is and how it works](../docs/DOCUMENTATION.md#validator) * [More about template and how to use it](../docs/DOCUMENTATION.md#template) ### Results Whatever you want. [Error flag](../docs/DOCUMENTATION.md#anyerrors), compact [list of codes](../docs/DOCUMENTATION.md#codes), or detailed maps of [messages](../docs/DOCUMENTATION.md#messagemap) and [codes](../docs/DOCUMENTATION.md#codemap). With sugar on top: friendly [ToString() printing](../docs/DOCUMENTATION.md#tostring) that contains everything, nicely formatted. ``` csharp var validationResult = validator.Validate(signUpModel); if (validationResult.AnyErrors) { // check if a specific code has been recorded for Email property: if (validationResult.CodeMap["Email"].Contains("DOMAIN_BANNED")) { _actions.NotifyAboutDomainBanned(signUpModel.Email); } var errorsPrinting = validationResult.ToString(); // save all messages and codes printing into the logs _logger.LogError("Errors in incoming SignUpModel: {errors}", errorsPrinting); // return all error codes to the frontend return new SignUpActionResult { Success = false, ErrorCodes = validationResult.Codes, }; } ``` * [Validation result types](../docs/DOCUMENTATION.md#result) ### Rules Tons of [rules available out of the box](../docs/DOCUMENTATION.md#rules). Plus, an easy way to [define your own](../docs/DOCUMENTATION.md#custom-rules) with the full support of Validot internal features like [formattable message arguments](../docs/DOCUMENTATION.md#message-arguments). ``` csharp public static IRuleOut ExactLinesCount(this IRuleIn @this, int count) { return @this.RuleTemplate( value => value.Split(Environment.NewLine).Length == count, "Must contain exactly {count} lines", Arg.Number("count", count) ); } ``` ``` csharp .ExactLinesCount(4) // Must contain exactly 4 lines .ExactLinesCount(4).WithMessage("Required lines count: {count}") // Required lines count: 4 .ExactLinesCount(4).WithMessage("Required lines count: {count|format=000.00|culture=pl-PL}") // Required lines count: 004,00 ``` * [List of built-in rules](../docs/DOCUMENTATION.md#rules) * [Writing custom rules](../docs/DOCUMENTATION.md#custom-rules) * [Message arguments](../docs/DOCUMENTATION.md#message-arguments) ### Translations Pass errors directly to the end-users in the language of your application. ``` csharp Specification specification = s => s .Member(m => m.PrimaryEmail, m => m.Email()) .Member(m => m.Name, m => m.LengthBetween(3, 50)); var validator = Validator.Factory.Create(specification, settings => settings.WithPolishTranslation()); var model = new UserModel() { PrimaryEmail = "in@lid_em@il", Name = "X" }; var result = validator.Validate(model); result.ToString(); // Email: Must be a valid email address // Name: Must be between 3 and 50 characters in length result.ToString(translationName: "Polish"); // Email: Musi być poprawnym adresem email // Name: Musi być długości pomiędzy 3 a 50 znaków ``` At the moment Validot delivers the following translations out of the box: [Polish](../docs/DOCUMENTATION.md#withpolishtranslation), [Spanish](../docs/DOCUMENTATION.md#withspanishtranslation), [Russian](../docs/DOCUMENTATION.md#withrussiantranslation), [Portuguese](../docs/DOCUMENTATION.md#withportuguesetranslation) and [German](../docs/DOCUMENTATION.md#withgermantranslation). * [How translations work](../docs/DOCUMENTATION.md#translations) * [Custom translation](../docs/DOCUMENTATION.md#custom-translation) * [How to selectively override built-in error messages](../docs/DOCUMENTATION.md#overriding-messages) ### Dependency injection Although Validot doesn't contain direct support for the dependency injection containers (because it aims to rely solely on the .NET Standard 2.0), it includes helpers that can be used with any DI/IoC system. For example, if you're working with ASP.NET Core and looking for an easy way to register all of your validators with a single call (something like `services.AddValidators()`), wrap your specifications in the [specification holders](../docs/DOCUMENTATION.md#specification-holder), and use the following snippet: ``` csharp public void ConfigureServices(IServiceCollection services) { // ... registering other dependencies ... // Registering Validot's validators from the current domain's loaded assemblies var holderAssemblies = AppDomain.CurrentDomain.GetAssemblies(); var holders = Validator.Factory.FetchHolders(holderAssemblies) .GroupBy(h => h.SpecifiedType) .Select(s => new { ValidatorType = s.First().ValidatorType, ValidatorInstance = s.First().CreateValidator() }); foreach (var holder in holders) { services.AddSingleton(holder.ValidatorType, holder.ValidatorInstance); } // ... registering other dependencies ... } ``` * [What specification holders are and how to create them](../docs/DOCUMENTATION.md#specification-holder) * [Fetching specification holders from assemblies](../docs/DOCUMENTATION.md#fetching-holders) * [Writing the fully-featured `AddValidators` extension step-by-step](../docs/DOCUMENTATION.md#dependency-injection) ## Validot vs FluentValidation A short statement to start with - [@JeremySkinner](https://twitter.com/JeremySkinner)'s [FluentValidation](https://fluentvalidation.net/) is an excellent piece of work and has been a huge inspiration for this project. True, you can call Validot a direct competitor, but it differs in some fundamental decisions, and lot of attention has been focused on entirely different aspects. If - after reading this section - you think you can bear another approach, api and [limitations](#fluentValidations-features-that-validot-is-missing), at least give Validot a try. You might be positively surprised. Otherwise, FluentValidation is a good, safe choice, as Validot is certainly less hackable, and achieving some particular goals might be either difficult or impossible. ### Validot is faster and consumes less memory This document shows oversimplified results of [BenchmarkDotNet](https://benchmarkdotnet.org/) execution, but the intention is to present the general trend only. To have truly reliable numbers, I highly encourage you to [run the benchmarks yourself](../docs/DOCUMENTATION.md#benchmarks). There are three data sets, 10k models each; `ManyErrors` (every model has many errors), `HalfErrors` (circa 60% have errors, the rest are valid), `NoErrors` (all are valid) and the rules reflect each other as much as technically possible. I did my best to make sure that the tests are just and adequate, but I'm a human being and I make mistakes. Really, if you spot errors [in the code](https://github.com/bartoszlenar/Validot/tree/cdca31a2588bf801288ef73e8ca50bfd33be8049/tests/Validot.Benchmarks), framework usage, applied methodology... or if you can provide any counterexample proving that Validot struggles with some particular scenarios - I'd be very very very happy to accept a PR and/or discuss it on [GitHub Issues](https://github.com/bartoszlenar/Validot/issues). To the point; the statement in the header is true, but it doesn't come for free. Wherever possible and justified, Validot chooses performance and less allocations over [flexibility and extra features](#fluentvalidations-features-that-validot-is-missing). Fine with that kind of trade-off? Good, because the validation process in Validot might be **~1.6x faster while consuming ~4.7x less memory** (in the most representational, `Validate` tests using `HalfErrors` data set). Especially when it comes to memory consumption, Validot could be even 13.3x better than FluentValidation (`IsValid` tests with `HalfErrors` data set) . What's the secret? Read my blog post: [Validot's performance explained](https://lenar.dev/posts/validots-performance-explained). | Test | Data set | Library | Mean [ms] | Allocated [MB] | | - | - | - | -: | -: | | Validate | `ManyErrors` | FluentValidation | `703.83` | `453` | | Validate | `ManyErrors` | Validot | `307.04` | `173` | | FailFast | `ManyErrors` | FluentValidation | `21.63` | `21` | | FailFast | `ManyErrors` | Validot | `16.76` | `32` | | Validate | `HalfErrors` | FluentValidation | `563.92` | `362` | | Validate | `HalfErrors` | Validot | `271.62` | `81` | | FailFast | `HalfErrors` | FluentValidation | `374.90` | `249` | | FailFast | `HalfErrors` | Validot | `173.41` | `62` | | Validate | `NoErrors` | FluentValidation | `559.77` | `354` | | Validate | `NoErrors` | Validot | `260.99` | `75` | * [Validate benchmark](../tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs) - objects are validated. * [FailFast benchmark](../tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs) - objects are validated, the process stops on the first error. FluentValidation's `IsValid` is a property that wraps a simple check whether the validation result contains errors or not. Validot has [AnyErrors](../docs/DOCUMENTATION.md#anyerrors) that acts the same way, and [IsValid](../docs/DOCUMENTATION.md#isvalid) is a special mode that doesn't care about anything else but the first rule predicate that fails. If the mission is only to verify the incoming model whether it complies with the rules (discarding all of the details), this approach proves to be better up to one order of magnitude: | Test | Data set | Library | Mean [ms] | Allocated [MB] | | - | - | - | -: | -: | | IsValid | `ManyErrors` | FluentValidation | `20.91` | `21` | | IsValid | `ManyErrors` | Validot | `8.21` | `6` | | IsValid | `HalfErrors` | FluentValidation | `367.59` | `249` | | IsValid | `HalfErrors` | Validot | `106.77` | `20` | | IsValid | `NoErrors` | FluentValidation | `513.12` | `354` | | IsValid | `NoErrors` | Validot | `136.22` | `24` | * [IsValid benchmark](../tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs) - objects are validated, but only to check if they are valid or not. Combining these two methods in most cases could be quite beneficial. At first, [IsValid](../docs/DOCUMENTATION.md#isvalid) quickly verifies the object, and if it contains errors - only then [Validate](../docs/DOCUMENTATION.md#validate) is executed to report the details. Of course in some extreme cases (megabyte-size data? millions of items in the collection? dozens of nested levels with loops in reference graphs?) traversing through the object twice could neglect the profit. Still, for the regular web api input validation, it will undoubtedly serve its purpose: ``` csharp if (!validator.IsValid(model)) { errorMessages = validator.Validate(model).ToString(); } ``` | Test | Data set | Library | Mean [ms] | Allocated [MB] | | - | - | - | -: | -: | | Reporting | `ManyErrors` | FluentValidation | `768.00` | `464` | | Reporting | `ManyErrors` | Validot | `379.50` | `294` | | Reporting | `HalfErrors` | FluentValidation | `592.50` | `363` | | Reporting | `HalfErrors` | Validot | `294.60` | `76` | * [Reporting benchmark](../tests/Validot.Benchmarks/Comparisons/ReportingBenchmark.cs): * FluentValidation validates model, and `ToString()` is called if errors are detected. * Validot processes the model twice - at first, with its special mode, [IsValid](../docs/DOCUMENTATION.md#isvalid). Secondly - in case of errors detected - with the standard method, gathering all errors and printing them with `ToString()`. Benchmarks environment: Validot 2.3.0, FluentValidation 11.2.0, .NET 6.0.7, i7-9750H (2.60GHz, 1 CPU, 12 logical and 6 physical cores), X64 RyuJIT, macOS Monterey. ### Validot handles nulls on its own In Validot, null is a special case [handled by the core engine](../docs/DOCUMENTATION.md#null-policy). You don't need to secure the validation logic from null as your predicate will never receive it. ``` csharp Member(m => m.LastName, m => m .Rule(lastName => lastName.Length < 50) // 'lastName' is never null .Rule(lastName => lastName.All(char.IsLetter)) // 'lastName' is never null ) ``` ### Validot treats null as an error by default All values are marked as required by default. In the above example, if `LastName` member were null, the validation process would exit `LastName` scope immediately only with this single error message: ``` LastName: Required ``` The content of the message is, of course, [customizable](../docs/DOCUMENTATION.md#withmessage). If null should be allowed, place [Optional](../docs/DOCUMENTATION.md#optional) command at the beginning: ``` csharp Member(m => m.LastName, m => m .Optional() .Rule(lastName => lastName.Length < 50) // 'lastName' is never null .Rule(lastName => lastName.All(char.IsLetter)) // 'lastName' is never null ) ``` Again, no rule predicate is triggered. Also, null `LastName` member doesn't result with errors. * [Read more about how Validot handles nulls](../docs/DOCUMENTATION.md#null-policy) ### Validot's Validator is immutable Once [validator](../docs/DOCUMENTATION.md#validator) instance is created, you can't modify its internal state or [settings](../docs/DOCUMENTATION.md#settings). If you need the process to fail fast (FluentValidation's `CascadeMode.Stop`), use the flag: ``` csharp validator.Validate(model, failFast: true); ``` ### FluentValidation's features that Validot is missing Features that might be in the scope and are technically possible to implement in the future: * failing fast only in a single scope ([discuss it on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/5)) * validated value in the error message ([discuss it on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/6)) Features that are very unlikely to be in the scope as they contradict the project's principles, and/or would have a very negative impact on performance, and/or are impossible to implement: * Full integration with ASP.NET or other frameworks: * Validot tries to remain a single-purpose library, depending only on .NET Standard 2.0. Thus all integrations need to be done individually. * However, Validot delivers [FetchHolders method](../docs/DOCUMENTATION.md#fetching-holders) that makes such integrations possible to wrap within a few lines of code. The quick example is in the [Dependency Injection section of this readme file](#dependency-injection), more advanced solution with explanation is contained [in the documentation](../docs/DOCUMENTATION.md#dependency-injection). * Access to any stateful context in the rule condition predicate: * It implicates a lack of support for dynamic message content and/or amount. * Callbacks: * Please react on [failure/success](../docs/DOCUMENTATION.md#anyerrors) after getting [validation result](../docs/DOCUMENTATION.md#result). * Pre-validation: * All cases can be handled by additional validation and a proper if-else. * Also, the problem of the root being null doesn't exist in Validot (it's a regular case, [covered entirely with fluent api](../docs/DOCUMENTATION.md#presence-commands)) * Rule sets * workaround; multiple [validators](../docs/DOCUMENTATION.md#validator) for different parts of the object. * `await`/`async` support * only support for large collections is planned ([more details on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/2)) * severities ([more details on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/4)) * workaround: multiple [validators](../docs/DOCUMENTATION.md#validator) for error groups with different severities. ## Project info ### Requirements Validot is a dotnet class library targeting .NET Standard 2.0. There are no extra dependencies. Please check the [official Microsoft document](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) that lists all the platforms that can use it on. ### Versioning [Semantic versioning](https://semver.org/) is being used very strictly. The major version is updated only when there is a breaking change, no matter how small it might be (e.g., adding extra method to the public interface). On the other hand, a huge pack of new features will bump the minor version only. Before every major version update, at least one preview version is published. ### Reliability Unit tests coverage hits 100% very close, and it can be detaily verified on [codecov.io](https://codecov.io/gh/bartoszlenar/Validot/branch/main). Before publishing, each release is tested on the ["latest" version](https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners#supported-runners-and-hardware-resources) of the following operating systems: * macOS * Ubuntu * Windows Server using the upcoming, the current and all also the supported [LTS versions](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) of the underlying frameworks: * .NET 8.0 * .NET 6.0 * .NET Framework 4.8 (Windows 2019 only) ### Performance Benchmarks exist in the form of [the console app project](https://github.com/bartoszlenar/Validot/tree/5219a8da7cc20cd5b9c5c49dd5c0940e829f6fe9/tests/Validot.Benchmarks) based on [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet). Also, you can trigger performance tests [from the build script](../docs/DOCUMENTATION.md#benchmarks). ### Documentation The documentation is hosted alongside the source code, in the git repository, as a single markdown file: [DOCUMENTATION.md](./../docs/DOCUMENTATION.md). Code examples from the documentation live as [functional tests](https://github.com/bartoszlenar/Validot/tree/132842cff7c5097c1cad8e762df094e74bb6038c/tests/Validot.Tests.Functional). ### Development The entire project ([source code](https://github.com/bartoszlenar/Validot), [issue tracker](https://github.com/bartoszlenar/Validot/issues), [documentation](../docs/DOCUMENTATION.md), and [CI workflows](https://github.com/bartoszlenar/Validot/actions)) is hosted here on github.com. Any contribution is more than welcome. If you'd like to help, please don't forget to check out the [CONTRIBUTING](./../docs/CONTRIBUTING.md) file and [issues page](https://github.com/bartoszlenar/Validot/issues). ### Licencing Validot uses the [MIT license](../LICENSE). Long story short; you are more than welcome to use it anywhere you like, completely free of charge and without oppressive obligations. ================================================ FILE: .github/pull_request_template.md ================================================ ## Issue Related issue: # (required - if the issue doesn't exist, please create it first) ## Type of changes - [ ] Documentation update or other changes not related to the code. - [ ] Bug fix (non-breaking change which fixes an issue). - [ ] New feature (non-breaking change which adds functionality). - [ ] Breaking change (fix or feature that would cause existing functionality to change). ## Description * Please provide the PR description in points. * Include as many details as you can * What code areas are affected? * Any impact on performance? * Any problems with passing unit tests? ``` csharp // If applicable, please provide any code that demonstrates the changes (a new functionality in action, or a proof that the bug is fixed) ``` ## Tests * Please list in points all the details about testing the content of this PR * Any new/altered unit/functional tests? * Any new benchmarks? * Any new unit/functional tests? Any altered tests? ================================================ FILE: .github/workflows/CI.yml ================================================ name: CI on: push: branches-ignore: - "wip/**" pull_request: branches-ignore: - "wip/**" release: types: [published] jobs: tests: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] dotnet: ["8.0.x"] runs-on: ${{ matrix.os }} name: Test on ${{ matrix.os }} using dotnet ${{ matrix.dotnet }} steps: - name: Check out code uses: actions/checkout@v4 - name: Setup dotnet uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x ${{ matrix.dotnet }} - name: Init workflow variables run: pwsh .github/workflows/Init-WorkflowVariables.ps1 - name: Compile run: pwsh build.ps1 --target compile --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }} - name: Tests run: pwsh build.ps1 --target tests --skip --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }} - name: Upload artifact; details of failed tests uses: actions/upload-artifact@v4 if: failure() with: name: Validot.${{ env.VALIDOT_VERSION }}.${{ matrix.dotnet }}.${{ matrix.os }}.testresults path: artifacts/tests/Validot.${{ env.VALIDOT_VERSION }}.testresults tests_netframework: strategy: matrix: os: [windows-2019] dotnet: [net48] runs-on: ${{ matrix.os }} name: Test on ${{ matrix.os }} using dotnet ${{ matrix.dotnet }} steps: - name: Check out code uses: actions/checkout@v2 - name: Setup base dotnet sdk uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Init workflow variables run: pwsh .github/workflows/Init-WorkflowVariables.ps1 - name: Compile run: pwsh build.ps1 --target compile --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }} - name: Tests run: pwsh build.ps1 --target tests --skip --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }} - name: Upload artifact; details of failed tests uses: actions/upload-artifact@v4 if: failure() with: name: Validot.${{ env.VALIDOT_VERSION }}.${{ matrix.dotnet }}.${{ matrix.os }}.testresults path: artifacts/tests/Validot.${{ env.VALIDOT_VERSION }}.testresults code_coverage: needs: [tests, tests_netframework] if: github.event_name == 'release' || github.event_name == 'pull_request' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest name: Code coverage steps: - name: Check out code uses: actions/checkout@v2 - name: Setup base dotnet sdk uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x 6.0.x - name: Init workflow variables run: pwsh .github/workflows/Init-WorkflowVariables.ps1 - name: Analyze code coverage run: pwsh build.ps1 --target codecoveragereport --dotnet 6.0.x --configuration debug --version ${{ env.VALIDOT_VERSION }} - name: Upload artifact; code coverage data uses: actions/upload-artifact@v4 with: name: Validot.${{ env.VALIDOT_VERSION }}.opencover.xml path: artifacts/coverage/Validot.${{ env.VALIDOT_VERSION }}.opencover.xml - name: Upload artifact; code coverage summary uses: actions/upload-artifact@v4 with: name: Validot.${{ env.VALIDOT_VERSION }}.coverage_summary.json path: artifacts/coverage_reports/Validot.${{ env.VALIDOT_VERSION }}.coverage_summary.json - name: Upload artifact; code coverage report if: github.event_name == 'release' uses: actions/upload-artifact@v4 with: name: Validot.${{ env.VALIDOT_VERSION }}.coverage_report path: artifacts/coverage_reports/Validot.${{ env.VALIDOT_VERSION }}.coverage_report nuget_package: needs: [tests, tests_netframework] if: github.event_name == 'release' runs-on: ubuntu-latest name: NuGet package steps: - name: Checking out code uses: actions/checkout@v2 - name: Setup base dotnet sdk uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Init workflow variables run: pwsh .github/workflows/Init-WorkflowVariables.ps1 - name: Create package run: pwsh build.ps1 --target nugetpackage --dotnet 8.0.x --configuration release --commitsha ${{ github.sha }} --version ${{ env.VALIDOT_VERSION }} - name: Upload artifact; nuget package uses: actions/upload-artifact@v4 with: name: Validot.${{ env.VALIDOT_VERSION }}.nupkg path: artifacts/nuget/${{ env.VALIDOT_VERSION }}/Validot.${{ env.VALIDOT_VERSION }}.nupkg - name: Upload artifact; nuget package symbols uses: actions/upload-artifact@v4 with: name: Validot.${{ env.VALIDOT_VERSION }}.snupkg path: artifacts/nuget/${{ env.VALIDOT_VERSION }}/Validot.${{ env.VALIDOT_VERSION }}.snupkg - name: Publish nuget package run: pwsh build.ps1 --target publishnugetpackage --skip --dotnet 8.0.x --configuration release --version ${{ env.VALIDOT_VERSION }} --commitsha ${{ github.sha }} --nugetapikey ${{ secrets.NUGET_API_KEY }} release_assets: needs: [code_coverage, nuget_package] if: github.event_name == 'release' runs-on: ubuntu-latest name: Upload release assets steps: - name: Checking out code uses: actions/checkout@v2 - name: Init workflow variables run: pwsh .github/workflows/Init-WorkflowVariables.ps1 - name: Download artifact; nuget package uses: actions/download-artifact@v4.1.7 with: name: Validot.${{ env.VALIDOT_VERSION }}.nupkg path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.nuget - name: Download artifact; nuget package symbols uses: actions/download-artifact@v4.1.7 with: name: Validot.${{ env.VALIDOT_VERSION }}.snupkg path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.nuget - name: Download artifact; code coverage data uses: actions/download-artifact@v4.1.7 with: name: Validot.${{ env.VALIDOT_VERSION }}.opencover.xml path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage/data - name: Download artifact; code coverage summary uses: actions/download-artifact@v4.1.7 with: name: Validot.${{ env.VALIDOT_VERSION }}.coverage_summary.json path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage - name: Download artifact; code coverage report uses: actions/download-artifact@v4.1.7 with: name: Validot.${{ env.VALIDOT_VERSION }}.coverage_report path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage/report - name: Zip artifacts run: | cd artifacts zip -rX Validot.${{ env.VALIDOT_VERSION }}.nuget.zip Validot.${{ env.VALIDOT_VERSION }}.nuget zip -rX Validot.${{ env.VALIDOT_VERSION }}.coverage.zip Validot.${{ env.VALIDOT_VERSION }}.coverage - name: Upload release asset; nuget package with symbols uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.nuget.zip asset_name: Validot.${{ env.VALIDOT_VERSION }}.nuget.zip asset_content_type: application/zip - name: Upload release asset; code coverage data and reports uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage.zip asset_name: Validot.${{ env.VALIDOT_VERSION }}.coverage.zip asset_content_type: application/zip ================================================ FILE: .github/workflows/Init-WorkflowVariables.ps1 ================================================ $commitShortSha = $env:GITHUB_SHA.Substring(0, 7) if ($env:GITHUB_EVENT_NAME.Equals("release")) { $tag = $env:GITHUB_REF.Substring("refs/tags/".Length).TrimStart('v'); if ($tag -match "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") { $version = $tag } else { Write-Error "Tag contains invalid semver: $tag" -ErrorAction Stop } } else { $version = $commitShortSha } "VALIDOT_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "VALIDOT_COMMIT=$commitShortSha" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "VALIDOT_CI=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/csharp # Edit at https://www.gitignore.io/?templates=csharp ### Csharp ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # End of https://www.gitignore.io/api/csharp # Visual Studio Code .vscode/ # Tools tools/ # jetBrains Rider .idea/ # MacOS .DS_Store ================================================ FILE: .nuke/build.schema.json ================================================ { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/build", "title": "Build Schema", "definitions": { "build": { "type": "object", "properties": { "AllowWarnings": { "type": "boolean", "description": "Allow warnings" }, "BenchmarksFilter": { "type": "string", "description": "Benchmark filter. If empty, all benchmarks will be run" }, "CodeCovApiKey": { "type": "string", "description": "CodeCov API key, allows to publish code coverage" }, "CommitSha": { "type": "string", "description": "Commit SHA" }, "Configuration": { "type": "string", "description": "Configuration to build. 'Debug' (default) or 'Release'", "enum": [ "Debug", "Release" ] }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, "DotNet": { "type": "string", "description": "dotnet framework id or SDK version (if SDK version is provided, the highest framework available is selected). Default value is 'netcoreapp3.1'" }, "FullBenchmark": { "type": "boolean", "description": "If true, BenchmarkDotNet will run full (time consuming, but more accurate) jobs" }, "Help": { "type": "boolean", "description": "Shows the help text for this build assembly" }, "Host": { "type": "string", "description": "Host for execution. Default is 'automatic'", "enum": [ "AppVeyor", "AzurePipelines", "Bamboo", "Bitbucket", "Bitrise", "GitHubActions", "GitLab", "Jenkins", "Rider", "SpaceAutomation", "TeamCity", "Terminal", "TravisCI", "VisualStudio", "VSCode" ] }, "NoLogo": { "type": "boolean", "description": "Disables displaying the NUKE logo" }, "NuGetApi": { "type": "string", "description": "NuGet API. Where to publish NuGet package. Default value is 'https://api.nuget.org/v3/index.json'" }, "NuGetApiKey": { "type": "string", "description": "NuGet API key, allows to publish NuGet package" }, "Partition": { "type": "string", "description": "Partition to use on CI" }, "Plan": { "type": "boolean", "description": "Shows the execution plan (HTML)" }, "Profile": { "type": "array", "description": "Defines the profiles to load", "items": { "type": "string" } }, "Root": { "type": "string", "description": "Root directory during build execution" }, "Skip": { "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { "type": "string", "enum": [ "AddTranslation", "Benchmarks", "Clean", "CodeCoverage", "CodeCoverageReport", "Compile", "CompileProject", "CompileTests", "NugetPackage", "PublishCodeCoverage", "PublishNugetPackage", "Reset", "Restore", "Tests" ] } }, "Solution": { "type": "string", "description": "Path to a solution file that is automatically loaded" }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { "type": "string", "enum": [ "AddTranslation", "Benchmarks", "Clean", "CodeCoverage", "CodeCoverageReport", "Compile", "CompileProject", "CompileTests", "NugetPackage", "PublishCodeCoverage", "PublishNugetPackage", "Reset", "Restore", "Tests" ] } }, "TranslationName": { "type": "string", "description": "(only for target AddTranslation) Translation name" }, "Verbosity": { "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", "enum": [ "Minimal", "Normal", "Quiet", "Verbose" ] }, "Version": { "type": "string", "description": "Version. Default value is '0.0.0-timestamp'" } } } } } ================================================ FILE: .nuke/parameters.json ================================================ { "$schema": "./build.schema.json", "Solution": "Validot.sln" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020-2021 Bartosz Lenar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Validot.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{013FC1C3-9AFF-43CE-915C-6BF4FA32FE80}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot", "src\Validot\Validot.csproj", "{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.Tests.Unit", "tests\Validot.Tests.Unit\Validot.Tests.Unit.csproj", "{6F71A1C1-319A-492F-B930-0FF4FB599918}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.Tests.Functional", "tests\Validot.Tests.Functional\Validot.Tests.Functional.csproj", "{E5AC95A3-1613-433C-A318-9A19B24BF73F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.Benchmarks", "tests\Validot.Benchmarks\Validot.Benchmarks.csproj", "{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.MemoryLeak", "tests\Validot.MemoryLeak\Validot.MemoryLeak.csproj", "{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{4BDB92C9-0BA6-4740-A177-18E80611976A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyWithHolders", "tests\AssemblyWithHolders\AssemblyWithHolders.csproj", "{971FACBC-AF7F-4897-A081-947C6F2A864A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4BDB92C9-0BA6-4740-A177-18E80611976A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4BDB92C9-0BA6-4740-A177-18E80611976A}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x64.ActiveCfg = Debug|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x64.Build.0 = Debug|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x86.ActiveCfg = Debug|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x86.Build.0 = Debug|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|Any CPU.Build.0 = Release|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x64.ActiveCfg = Release|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x64.Build.0 = Release|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x86.ActiveCfg = Release|Any CPU {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x86.Build.0 = Release|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x64.ActiveCfg = Debug|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x64.Build.0 = Debug|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x86.ActiveCfg = Debug|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x86.Build.0 = Debug|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|Any CPU.Build.0 = Release|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x64.ActiveCfg = Release|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x64.Build.0 = Release|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x86.ActiveCfg = Release|Any CPU {6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x86.Build.0 = Release|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x64.ActiveCfg = Debug|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x64.Build.0 = Debug|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x86.ActiveCfg = Debug|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x86.Build.0 = Debug|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|Any CPU.Build.0 = Release|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x64.ActiveCfg = Release|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x64.Build.0 = Release|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x86.ActiveCfg = Release|Any CPU {E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x86.Build.0 = Release|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x64.ActiveCfg = Debug|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x64.Build.0 = Debug|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x86.ActiveCfg = Debug|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x86.Build.0 = Debug|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|Any CPU.Build.0 = Release|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x64.ActiveCfg = Release|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x64.Build.0 = Release|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x86.ActiveCfg = Release|Any CPU {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x86.Build.0 = Release|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x64.ActiveCfg = Debug|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x64.Build.0 = Debug|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x86.ActiveCfg = Debug|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x86.Build.0 = Debug|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|Any CPU.Build.0 = Release|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x64.ActiveCfg = Release|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x64.Build.0 = Release|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x86.ActiveCfg = Release|Any CPU {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x86.Build.0 = Release|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|Any CPU.Build.0 = Debug|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x64.ActiveCfg = Debug|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x64.Build.0 = Debug|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x86.ActiveCfg = Debug|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x86.Build.0 = Debug|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|Any CPU.ActiveCfg = Release|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|Any CPU.Build.0 = Release|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x64.ActiveCfg = Release|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x64.Build.0 = Release|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x86.ActiveCfg = Release|Any CPU {971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22} = {013FC1C3-9AFF-43CE-915C-6BF4FA32FE80} {6F71A1C1-319A-492F-B930-0FF4FB599918} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176} {E5AC95A3-1613-433C-A318-9A19B24BF73F} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176} {D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176} {1030576B-DAC1-46C1-A6E3-6E809B5ED0EE} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176} {971FACBC-AF7F-4897-A081-947C6F2A864A} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176} EndGlobalSection EndGlobal ================================================ FILE: build/.editorconfig ================================================ [*.cs] dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning dotnet_style_require_accessibility_modifiers = never:warning csharp_style_expression_bodied_methods = true:silent csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_accessors = true:warning dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA2211.severity = none dotnet_diagnostic.CA1050.severity = none dotnet_diagnostic.CA1847.severity = none dotnet_diagnostic.CA1845.severity = none dotnet_diagnostic.IDE0022.severity = none dotnet_diagnostic.IDE0051.severity = none dotnet_diagnostic.IDE0058.severity = none ================================================ FILE: build/Build.cs ================================================ #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. #pragma warning disable IDE0057 // Use range operator #pragma warning disable CA1852 // sealed class #pragma warning disable CA1865 // Use char overload using System; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Nuke.Common; using Nuke.Common.CI; using Nuke.Common.IO; using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Utilities.Collections; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; [ShutdownDotNetAfterServerBuild] class Build : NukeBuild { static readonly Regex SemVerRegex = new Regex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", RegexOptions.Compiled); static readonly Regex TargetFrameworkRegex = new Regex(@".+<\/TargetFramework>", RegexOptions.Compiled); static readonly DateTimeOffset BuildTime = DateTimeOffset.UtcNow; static readonly string DefaultFrameworkId = "net8.0"; public static int Main() => Execute(x => x.Compile); [Parameter("Configuration to build. 'Debug' (default) or 'Release'.")] readonly Configuration Configuration = Configuration.Debug; [Parameter("dotnet framework id or SDK version (if SDK version is provided, the highest framework available is selected). Default value is 'netcoreapp3.1'")] string DotNet; [Parameter("Version. Default value is '0.0.0-timestamp'")] string Version; [Parameter("NuGet API. Where to publish NuGet package. Default value is 'https://api.nuget.org/v3/index.json'")] readonly string NuGetApi = "https://api.nuget.org/v3/index.json"; [Parameter("NuGet API key, allows to publish NuGet package.")] readonly string NuGetApiKey; [Parameter("CodeCov API key, allows to publish code coverage.")] readonly string CodeCovApiKey; [Parameter("Commit SHA")] readonly string CommitSha; [Parameter("If true, BenchmarkDotNet will run full (time consuming, but more accurate) jobs.")] readonly bool FullBenchmark; [Parameter("Benchmark filter. If empty, all benchmarks will be run.")] readonly string BenchmarksFilter; [Parameter("Allow warnings")] readonly bool AllowWarnings; [Parameter("(only for target AddTranslation) Translation name")] readonly string TranslationName; [Solution] readonly Solution Solution; AbsolutePath SourceDirectory => RootDirectory / "src"; AbsolutePath TestsDirectory => RootDirectory / "tests"; AbsolutePath ToolsPath => RootDirectory / "tools"; AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; AbsolutePath TestsResultsDirectory => ArtifactsDirectory / "tests"; AbsolutePath CodeCoverageDirectory => ArtifactsDirectory / "coverage"; AbsolutePath CodeCoverageReportsDirectory => ArtifactsDirectory / "coverage_reports"; AbsolutePath BenchmarksDirectory => ArtifactsDirectory / "benchmarks"; AbsolutePath NuGetDirectory => ArtifactsDirectory / "nuget"; protected override void OnBuildInitialized() { base.OnBuildCreated(); DotNet = GetFramework(DotNet); Logger.Info($"DotNet: {DotNet}"); Version = GetVersion(Version); Logger.Info($"Version: {Version}"); Logger.Info($"NuGetApi: {NuGetApi ?? "MISSING"}"); Logger.Info($"Configuration: {Configuration}"); Logger.Info($"CommitSha: {CommitSha ?? "MISSING"}"); Logger.Info($"AllowWarnings: {AllowWarnings}"); Logger.Info($"FullBenchmark: {FullBenchmark}"); Logger.Info($"BenchmarkFilter: {FullBenchmark}"); var nuGetApiKeyPresence = (NuGetApiKey is null) ? "MISSING" : "present"; Logger.Info($"NuGetApiKey: {nuGetApiKeyPresence}"); var codeCovApiKeyPresence = (CodeCovApiKey is null) ? "MISSING" : "present"; Logger.Info($"CodeCovApiKey: {codeCovApiKeyPresence}"); SetFrameworkInTests(DotNet); SetVersionInAssemblyInfo(Version, CommitSha); } protected override void OnBuildFinished() { ResetFrameworkInTests(); ResetVersionInAssemblyInfo(); base.OnBuildFinished(); } Target AddTranslation => _ => _ .Requires(() => TranslationName) .Executes(() => { CreateFromTemplate(SourceDirectory / "Validot" / "Translations" / "_Template"); CreateFromTemplate(TestsDirectory / "Validot.Tests.Unit" / "Translations" / "_Template"); void CreateFromTemplate(AbsolutePath templatePath) { CopyDirectoryRecursively(templatePath, templatePath.Parent / TranslationName); var files = new DirectoryInfo(templatePath.Parent / TranslationName).GetFiles(); foreach (var file in files) { var finalFilePath = file.FullName.Replace("_Template", TranslationName).Replace(".txt", string.Empty); RenameFile(file.FullName, finalFilePath); File.WriteAllText(finalFilePath, File.ReadAllText(finalFilePath).Replace("_Template", TranslationName)); } } }); Target Reset => _ => _ .Executes(() => { EnsureCleanDirectory(TemporaryDirectory); EnsureCleanDirectory(ArtifactsDirectory); EnsureCleanDirectory(ToolsPath); ResetFrameworkInTests(); }) .Triggers(Clean); Target Clean => _ => _ .Before(Restore) .Executes(() => { SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); }); Target Restore => _ => _ .Executes(() => { DotNetRestore(_ => _ .SetProjectFile(Solution)); }); Target CompileProject => _ => _ .DependsOn(Clean, Restore) .Executes(() => { DotNetBuild(c => c .EnableNoRestore() .SetTreatWarningsAsErrors(!AllowWarnings) .SetProjectFile(SourceDirectory / "Validot/Validot.csproj") .SetConfiguration(Configuration) .SetFramework("netstandard2.0") ); }); Target CompileTests => _ => _ .DependsOn(Clean, Restore) .After(CompileProject) .Executes(() => { var testsProjects = new[] { TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj", TestsDirectory / "Validot.Tests.Functional/Validot.Tests.Functional.csproj" }; foreach (var testProject in testsProjects) { DotNetBuild(c => c .EnableNoRestore() .SetTreatWarningsAsErrors(!AllowWarnings) .SetProjectFile(testProject) .SetConfiguration(Configuration) .SetFramework(DotNet) .AddProperty("DisableSourceLink", "1") ); } }); Target Compile => _ => _ .DependsOn(CompileProject, CompileTests); Target Tests => _ => _ .DependsOn(CompileTests) .Executes(() => { DotNetTest(p => p .EnableNoBuild() .SetConfiguration(Configuration) .SetProjectFile(TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj") .SetFramework(DotNet) .SetLoggers($"junit;LogFilePath={TestsResultsDirectory / $"Validot.{Version}.testresults" / $"Validot.{Version}.unit.junit"}") ); DotNetTest(p => p .EnableNoBuild() .SetConfiguration(Configuration) .SetProjectFile(TestsDirectory / "Validot.Tests.Functional/Validot.Tests.Functional.csproj") .SetFramework(DotNet) .SetLoggers($"junit;LogFilePath={TestsResultsDirectory / $"Validot.{Version}.testresults" / $"Validot.{Version}.functional.junit"}") ); }); Target CodeCoverage => _ => _ .DependsOn(CompileTests) .Requires(() => Configuration == Configuration.Debug) .Executes(() => { var reportFile = CodeCoverageDirectory / $"Validot.{Version}.opencover.xml"; DotNetTest(p => p .EnableNoBuild() .SetProjectFile(TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj") .SetConfiguration(Configuration.Debug) .SetFramework(DotNet) .AddProperty("CollectCoverage", "true") .AddProperty("CoverletOutput", reportFile) .AddProperty("CoverletOutputFormat", "opencover") .AddProperty("DisableSourceLink", "1") ); Logger.Info($"CodeCoverage opencover format file location: {reportFile} ({new FileInfo(reportFile).Length} bytes)"); }); Target CodeCoverageReport => _ => _ .DependsOn(CodeCoverage) .Requires(() => Configuration == Configuration.Debug) .Executes(() => { var toolPath = InstallAndGetToolPath("dotnet-reportgenerator-globaltool", "4.8.1", "ReportGenerator.dll", "net5.0"); var toolParameters = new[] { $"-reports:{CodeCoverageDirectory / $"Validot.{Version}.opencover.xml"}", $"-reporttypes:HtmlInline_AzurePipelines;JsonSummary", $"-targetdir:{CodeCoverageReportsDirectory / $"Validot.{Version}.coverage_report"}", $"-historydir:{CodeCoverageReportsDirectory / "_history"}", $"-title:Validot unit tests code coverage report", $"-tag:v{Version}" + (CommitSha is null ? "" : $"+{CommitSha}"), }; ExecuteTool(toolPath, string.Join(" ", toolParameters.Select(p => $"\"{p}\""))); File.Move(CodeCoverageReportsDirectory / $"Validot.{Version}.coverage_report/Summary.json", CodeCoverageReportsDirectory / $"Validot.{Version}.coverage_summary.json"); }); Target Benchmarks => _ => _ .DependsOn(Clean) .Executes(() => { var benchmarksPath = BenchmarksDirectory / $"Validot.{Version}.benchmarks"; var jobShort = FullBenchmark ? string.Empty : "--job short"; var filter = BenchmarksFilter is null ? "*" : BenchmarksFilter; DotNetRun(p => p .SetProjectFile(TestsDirectory / "Validot.Benchmarks/Validot.Benchmarks.csproj") .SetConfiguration(Configuration.Release) .SetProcessArgumentConfigurator(a => a .Add("--") .Add($"--artifacts {benchmarksPath} {jobShort}") .Add("--exporters GitHub StackOverflow JSON HTML") .Add($"--filter {filter}") ) ); }); Target NugetPackage => _ => _ .DependsOn(Compile) .Requires(() => Configuration == Configuration.Release) .Executes(() => { DotNetPack(p => p .EnableNoBuild() .SetConfiguration(Configuration.Release) .SetProject(SourceDirectory / "Validot/Validot.csproj") .SetVersion(Version) .SetOutputDirectory(NuGetDirectory / Version) ); }); Target PublishNugetPackage => _ => _ .DependsOn(NugetPackage) .Requires(() => NuGetApiKey != null) .Requires(() => Configuration == Configuration.Release) .Executes(() => { DotNetNuGetPush(p => p .SetSource(NuGetApi) .SetApiKey(NuGetApiKey) .SetTargetPath(NuGetDirectory / Version / $"Validot.{Version}.nupkg") ); }); Target PublishCodeCoverage => _ => _ .DependsOn(CodeCoverage) .Requires(() => CodeCovApiKey != null) .Requires(() => Configuration == Configuration.Debug) .Executes(() => { var reportFile = CodeCoverageDirectory / $"Validot.{Version}.opencover.xml"; var toolPath = InstallAndGetToolPath("codecov.tool", "1.13.0", "codecov.dll", "net5.0"); var toolParameters = new[] { $"--sha {CommitSha}", $"--file {reportFile}", $"--token {CodeCovApiKey}", $"--required" }; ExecuteTool(toolPath, string.Join(" ", toolParameters)); }); void SetFrameworkInTests(string framework) { var testsCsprojs = new[] { TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj", TestsDirectory / "Validot.Tests.Functional/Validot.Tests.Functional.csproj", TestsDirectory / "Validot.Benchmarks/Validot.Benchmarks.csproj", }; foreach (var csproj in testsCsprojs) { SetFrameworkInCsProj(framework, csproj); } } void SetFrameworkInCsProj(string framework, string csProjPath) { Logger.Info($"Setting framework {framework} in {csProjPath}"); var content = TargetFrameworkRegex.Replace(File.ReadAllText(csProjPath), $"{framework}"); File.WriteAllText(csProjPath, content); } void SetVersionInAssemblyInfo(string version, string commitSha) { var assemblyVersion = "0.0.0.0"; var assemblyFileVersion = "0.0.0.0"; if (SemVerRegex.IsMatch(version)) { assemblyVersion = version.Substring(0, version.IndexOf(".", StringComparison.InvariantCulture)) + ".0.0.0"; assemblyFileVersion = version.Contains("-", StringComparison.InvariantCulture) ? version.Substring(0, version.IndexOf("-", StringComparison.InvariantCulture)) + ".0" : version + ".0"; } Logger.Info("Setting AssemblyVersion: " + assemblyVersion); Logger.Info("Setting AssemblyFileVersion: " + assemblyFileVersion); var assemblyInfoPath = SourceDirectory / "Validot/Properties/AssemblyInfo.cs"; var assemblyInfoLines = File.ReadAllLines(assemblyInfoPath); var autogeneratedPostfix = "// this line is autogenerated by the build script"; for (var i = 0; i < assemblyInfoLines.Length; ++i) { if (assemblyInfoLines[i].Contains("AssemblyVersion", StringComparison.InvariantCulture)) { assemblyInfoLines[i] = $"[assembly: System.Reflection.AssemblyVersion(\"{assemblyVersion}\")] {autogeneratedPostfix}"; } else if (assemblyInfoLines[i].Contains("AssemblyFileVersion", StringComparison.InvariantCulture)) { assemblyInfoLines[i] = $"[assembly: System.Reflection.AssemblyFileVersion(\"{assemblyFileVersion}\")] {autogeneratedPostfix}"; } } File.WriteAllLines(assemblyInfoPath, assemblyInfoLines); } void ResetVersionInAssemblyInfo() => SetVersionInAssemblyInfo("0.0.0", null); void ResetFrameworkInTests() => SetFrameworkInTests(DefaultFrameworkId); string GetFramework(string dotnet) { if (string.IsNullOrWhiteSpace(dotnet)) { Logger.Warn("DotNet: parameter not provided"); return DefaultFrameworkId; } if (char.IsDigit(dotnet.First())) { Logger.Info($"DotNet parameter recognized as SDK version: " + dotnet); if (dotnet.StartsWith("2.1.", StringComparison.Ordinal)) { return "netcoreapp2.1"; } if (dotnet.StartsWith("3.1.", StringComparison.Ordinal)) { return "netcoreapp3.1"; } if (dotnet.StartsWith("5.0.", StringComparison.Ordinal)) { return "net5.0"; } if (dotnet.StartsWith("6.0.", StringComparison.Ordinal)) { return "net6.0"; } if (dotnet.StartsWith("7.0.", StringComparison.Ordinal)) { return "net7.0"; } if (dotnet.StartsWith("8.0.", StringComparison.Ordinal)) { return "net8.0"; } Logger.Warn("Unrecognized dotnet SDK version: " + dotnet); return dotnet; } if (dotnet.StartsWith("netcoreapp", StringComparison.Ordinal) && dotnet["netcoreapp".Length..].All(c => char.IsDigit(c) || c == '.')) { Logger.Info("DotNet parameter recognized as .NET Core target: " + DotNet); return dotnet; } if (dotnet.StartsWith("net", StringComparison.Ordinal) && DotNet["net".Length..].All(char.IsDigit)) { Logger.Info("DotNet parameter recognized as .NET Framework target: " + dotnet); return dotnet; } Logger.Warn("Unrecognized dotnet framework id: " + dotnet); return dotnet; } string GetVersion(string version) { if (version is null) { Logger.Warn("Version: not provided."); return $"0.0.0-{BuildTime.DayOfYear}{BuildTime.ToString("HHmmss", CultureInfo.InvariantCulture)}"; } return version; } void ExecuteTool(string toolPath, string parameters) { ProcessTasks.StartProcess(ToolPathResolver.GetPathExecutable("dotnet"), toolPath + " -- " + parameters).AssertZeroExitCode(); } string InstallAndGetToolPath(string name, string version, string executableFileName, string framework = null) { var frameworkPart = framework is null ? $" (framework {framework})" : string.Empty; var toolStamp = $"{name} {version}{frameworkPart}, executable file: {executableFileName}"; Logger.Info($"Looking for tool: {toolStamp}"); var toolPath = ResolveToolPath(); if (toolPath is null) { DotNetToolInstall(c => c .SetPackageName(name) .SetVersion(version) .SetToolInstallationPath(ToolsPath) .SetGlobal(false)); } toolPath = ResolveToolPath(); if (toolPath is null) { Logger.Error($"Unable to find tool path: {name} {version} {executableFileName} {framework}"); } return toolPath; AbsolutePath ResolveToolPath() { var frameworkPart = framework != null ? (framework + "/**/") : string.Empty; Serilog.Log.Debug($"Looking for tool in {ToolsPath} using glob pattern: **/{name}/{version}/**/{frameworkPart}{executableFileName}"); var files = ToolsPath.GlobFiles($"**/{name}/{version}/**/{frameworkPart}{executableFileName}"); if (files.Count > 1) { foreach (var file in files) { Serilog.Log.Warning($"Found tool candidate: {file}"); } var toolPath = files.First(); Serilog.Log.Warning($"Found many tool candidates, so proceeding with the first one: {toolPath}"); return toolPath; } return files.FirstOrDefault(); } } } ================================================ FILE: build/Configuration.cs ================================================ using System; using System.ComponentModel; using System.Linq; using Nuke.Common.Tooling; [TypeConverter(typeof(TypeConverter))] public class Configuration : Enumeration { public static Configuration Debug = new Configuration { Value = nameof(Debug) }; public static Configuration Release = new Configuration { Value = nameof(Release) }; public static implicit operator string(Configuration configuration) { return configuration.Value; } } ================================================ FILE: build/_build.csproj ================================================ Exe net8.0 CS0649;CS0169 .. .. 1 ================================================ FILE: build/_build.csproj.DotSettings ================================================  DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW Implicit Implicit ExpressionBody 0 NEXT_LINE True False 120 IF_OWNER_IS_SINGLE_LINE WRAP_IF_LONG False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> True True True True True True True True True ================================================ FILE: build.cmd ================================================ :; set -eo pipefail :; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) :; ${SCRIPT_DIR}/build.sh "$@" :; exit $? @ECHO OFF powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* ================================================ FILE: build.ps1 ================================================ [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$BuildArguments ) Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### # CONFIGURATION ########################################################################### $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" $TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "STS" $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 $env:DOTNET_NOLOGO = 1 ########################################################################### # EXECUTION ########################################################################### function ExecSafe([scriptblock] $cmd) { & $cmd if ($LASTEXITCODE) { exit $LASTEXITCODE } } # If dotnet CLI is installed globally and it matches requested version, use for execution if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` $(dotnet --version) -and $LASTEXITCODE -eq 0) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) # If global.json exists, load expected version if (Test-Path $DotNetGlobalFile) { $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { $DotNetVersion = $DotNetGlobal.sdk.version } } # Install by channel or version $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" $env:PATH = "$DotNetDirectory;$env:PATH" } Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null } ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } ================================================ FILE: build.sh ================================================ #!/usr/bin/env bash bash --version 2>&1 | head -n 1 set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### # CONFIGURATION ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" DOTNET_CHANNEL="STS" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_NOLOGO=1 ########################################################################### # EXECUTION ########################################################################### function FirstJsonValue { perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" } # If dotnet CLI is installed globally and it matches requested version, use for execution if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then export DOTNET_EXE="$(command -v dotnet)" else # Download install script DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" mkdir -p "$TEMP_DIRECTORY" curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" chmod +x "$DOTNET_INSTALL_FILE" # If global.json exists, load expected version if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") if [[ "$DOTNET_VERSION" == "" ]]; then unset DOTNET_VERSION fi fi # Install by channel or version DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" if [[ -z ${DOTNET_VERSION+x} ]]; then "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" export PATH="$DOTNET_DIRECTORY:$PATH" fi echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true fi "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" ================================================ FILE: docs/CHANGELOG.md ================================================ # Changelog All notable changes to the [Validot project](https://github.com/bartoszlenar/Validot) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.5.0] - 2025-02-01 ### Added - Chinese translation. [#15](https://github.com/bartoszlenar/Validot/issues/15) [#37](https://github.com/bartoszlenar/Validot/pull/37) ## [2.5.0] - 2024-05-24 ### Added - Added `AsDictionary` command. [#26](https://github.com/bartoszlenar/Validot/issues/26) ## [2.4.1] - 2023-03-16 ### Fixed - Fixed invalid placeholders in messages. [#32](https://github.com/bartoszlenar/Validot/issues/32) ## [2.4.0] - 2022-10-01 ### Added - Added `AsType` command. [#4](https://github.com/bartoszlenar/Validot/issues/24) ### Fixed - Inline XML documentation for `AsConverted`. ## [2.3.0] - 2022-08-13 ### Added - Added `AsConverted` command. [#3](https://github.com/bartoszlenar/Validot/issues/3) ### Removed - Official support for not supported dotnet versions (.NET Core 2.1, .NET 5.0). ## [2.2.0] - 2021-11-05 ### Added - German translation (along with `WithGermanTranslation` extension to the settings builder). [#12](https://github.com/bartoszlenar/Validot/issues/12) - Portuguese translation (along with `WithPortugueseTranslation` extension to the settings builder). [#13](https://github.com/bartoszlenar/Validot/issues/13) ### Fixed - Fix to Spanish translation in `Times.BeforeOrEqualTo` message key. [#20](https://github.com/bartoszlenar/Validot/pull/20/commits/6a68dcdc17589f3c9bd524bc2266238b5245ff50) - Minor performance fixes and code improvements. [#21](https://github.com/bartoszlenar/Validot/pulls/21) [#22](https://github.com/bartoszlenar/Validot/pulls/22) ## [2.1.0] - 2021-06-07 ### Added - Spanish translation (along with `WithSpanishTranslation` extension to the settings builder). [#11](https://github.com/bartoszlenar/Validot/issues/11) - Russian translation (along with `WithRussianTranslation` extension to the settings builder). [#14](https://github.com/bartoszlenar/Validot/issues/14) - Translation template with script. To add a new translation all you need to do is call the build script, e.g. to add Korean, execute `pwsh build.ps1 --target AddTranslation --translationName Korean` (you can use `bash build.sh` instead of `pwsh build.ps1`) and the template with phrases will be created at `src/Validot/Translations/Korean` (plus unit tests in their own location). The only thing that is left to do is to enter translated phrases into the dictionary and make a PR! - A preview version of .NET 6 in the CI pipeline for all unit and functional tests. ## [2.0.0] - 2021-02-01 ### Added - `FetchHolders` method in the factory that helps [fetching specification holders](DOCUMENTATION.md#fetching-holders) from the assemblies and delivers a handy way to create the validators and [register them in the dependency injection containers](DOCUMENTATION.md#dependency-injection). [#10](https://github.com/bartoszlenar/Validot/issues/10) - Method in the factory that accepts the settings (in form of `IValidatorSettings`) directly, so the settings (e.g. from another validator) could be reused. This method compensates the lack of validator's public constructor. - [Settings holders](DOCUMENTATION.md#settings-holder) (`ISettingsHolder` interface), a mechanism similar to specification holders. This feature compensates the lack of `ITranslationHolder`. ### Fixed - Fixed inline XML code documentation, so it's visible from IDEs when referencing a nuget package. ### Changed - Introduced `IValidatorSettings` as a public interface for read-only access to the `ValidatorSettings` instance. `ValidatorSettings` class isn't public anymore, and validator's `Settings` property is now of type `IValidatorSettings`. This is a breaking change. - Renamed `ReferenceLoopProtection` flag to `ReferenceLoopProtectionEnabled`. This is a breaking change. - `IsValid` method uses a dedicated validation context that effectively doubles the speed of the operation - Ported all test projects to .NET 5. - Replaced ruleset-based code style rules with editorconfig and `Microsoft.CodeAnalysis.CSharp.CodeStyle` roslyn analyzers. - Renamed `master` git branch to `main`. ### Removed - Validator's public constructor. Please use the factory to create validators. If you want to reuse the settings, factory has new method that accepts `IValidatorSettings` instance. This is a breaking change. - Translation holders (`ITranslationHolder` interface). You can easily replace them with the newly introduced settings holders (`ISettingsHolder` interface). This is a breaking change. - CapacityInfo feature. It wasn't publicly available anyway and ultimately didn't prove to boost up the performance. ## [1.2.0] - 2020-11-04 ### Added - `And` - a fluent API method that [helps to visually separate the rules](DOCUMENTATION.md#And) within the specification. [#9](https://github.com/bartoszlenar/Validot/issues/9) - Inline documentation (XML comments) ## [1.1.0] - 2020-09-01 ### Added - Email rule now operates in two modes: ComplexRegex (which covers the previous, regex-based behavior, and is still set as default) and DataAnnotationsCompatible (compatible with the dotnet's [EmailAddressAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute?view=netcore-3.1)). - Title case support for the name argument, so name `SuperImportantValue123` when inserted with `{_name|format=titleCase}` is converted into `Super Important Value 123`. [#1](https://github.com/bartoszlenar/Validot/issues/1) ## [1.0.0] - 2020-06-23 First stable and public release. The reference point for all further changes. ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Contributing If you're reading this file, it means that you are - more or less - interested in contributing to the project. Before anything else, I'd like to say - thank you! It really means a lot to me that you consider this project to be worth your time. ## Flow 1. Let's have a discussion first! - It's really important that you visit the project's [Issues page](https://github.com/bartoszlenar/Validot/issues) and check if there is already an ongoing discussion around the same (or similar) idea. - Let's have a short chat about a feature that you have in mind (or a bug that you found), _before_ any actual code work. Validot is tweaked performance-wise and has a very sensitive codebase and for this reason I prefer to assign new features to myself by default. Of course, it's not a rule, but double please, let's have a chat about new features and breaking changes before you dedicate your time to Validot. 2. Fork and code! - How to build, run unit and functional tests? How to analyse code coverage a execute benchmarks? It's all covered in the [documentation](./DOCUMENTATION.md#Development). - Please get familiar with Validot's [project principles](#project-principles), [git](#git) and [code standards](#code-standards) - Provide changes in the documentation if needed. 3. Raise the PR! ## Project principles - Validot is not the ultimate solution to all validation scenarios and cases in the world. Let's keep it compact and simple, focused on a single problem. - Validot should not have any other dependencies than .NET Standard 2.0. - Validot - unless absolutely necessary - should not sacrifice performance for extra features. - Validot follows [semantic versioning](https://semver.org/) very strictly, no matter how annoying it could be. ## Code standards - Be aware that the code needs to compile and pass the tests on all of the LTS versions of .NET, under all supported OSes. - The CI system will let you know if your PR fails. - Please ensure that your code is covered with unit and functional tests. - Don't hesitate to ask - I'm more than happy to help you with everything! - CI system verifies the code style as well. - If your change is related with some core validation mechanism, please run the benchmarks to ensure it isn't affecting the performance. ## Git - The commits should follow the pattern of short notes in the past tense: - `Added Hungarian translation` - `Fixed IsValid bug #XXX` - Ideally, PR has a single commit with all changes, but that's not a requirement. As long as the each commit has logical sense and complies with all the rules - it compiles, passes tests, contains the related documentation changes - then it's fine. ## Translations - If you can help with expanding the list of built-it translations, that would be great! There is a build script helper there for you: - Type `pwsh build.ps1 --target AddTranslation --translationName Gibberlish` (of course, plese replace `Gibberlish` with your language name). - Navigate into `src/Validot/Translations/Gibberlish/GibberlishTranslation.cs` and replace the English phrases with their proper translations. - The script prepares everything, including `AddGibberlishTranslation()` settings extension and automatic unit tests. All you need to do next is to raise a PR. - You can replace `pwsh build.ps1` with `sh build.sh` or even execute windows command `bash.cmd`. It's all the same. ================================================ FILE: docs/DOCUMENTATION.md ================================================ # Documentation ## Table of contents - [Documentation](#documentation) - [Table of contents](#table-of-contents) - [Introduction](#introduction) - [Specification](#specification) - [Scope commands](#scope-commands) - [Parameter commands](#parameter-commands) - [Presence commands](#presence-commands) - [Error output](#error-output) - [Message](#message) - [Code](#code) - [Path](#path) - [Fluent api](#fluent-api) - [Rule](#rule) - [RuleTemplate](#ruletemplate) - [Member](#member) - [AsModel](#asmodel) - [AsCollection](#ascollection) - [AsNullable](#asnullable) - [AsConverted](#asconverted) - [AsType](#astype) - [AsDictionary](#asdictionary) - [WithCondition](#withcondition) - [WithPath](#withpath) - [WithMessage](#withmessage) - [WithExtraMessage](#withextramessage) - [WithCode](#withcode) - [WithExtraCode](#withextracode) - [Optional](#optional) - [Required](#required) - [Forbidden](#forbidden) - [And](#and) - [Null policy](#null-policy) - [Reference loop](#reference-loop) - [Validator](#validator) - [Validate](#validate) - [IsValid](#isvalid) - [Factory](#factory) - [Specification holder](#specification-holder) - [Settings holder](#settings-holder) - [Reusing settings](#reusing-settings) - [Fetching holders](#fetching-holders) - [Dependency injection](#dependency-injection) - [Settings](#settings) - [WithReferenceLoopProtection](#withreferenceloopprotection) - [WithTranslation](#withtranslation) - [Template](#template) - [Result](#result) - [AnyErrors](#anyerrors) - [Paths](#paths) - [Codes](#codes) - [CodeMap](#codemap) - [MessageMap](#messagemap) - [GetTranslatedMessageMap](#gettranslatedmessagemap) - [TranslationNames](#translationnames) - [ToString](#tostring) - [Rules](#rules) - [Global rules](#global-rules) - [Bool rules](#bool-rules) - [Char rules](#char-rules) - [Collections rules](#collections-rules) - [Numbers rules](#numbers-rules) - [Texts rules](#texts-rules) - [Times rules](#times-rules) - [Guid rules](#guid-rules) - [TimeSpan rules](#timespan-rules) - [Custom rules](#custom-rules) - [Message arguments](#message-arguments) - [Enum argument](#enum-argument) - [Guid argument](#guid-argument) - [Number argument](#number-argument) - [Text argument](#text-argument) - [Time argument](#time-argument) - [Translation argument](#translation-argument) - [Type argument](#type-argument) - [Path argument](#path-argument) - [Name argument](#name-argument) - [Translations](#translations) - [Built-in translations](#built-in-translations) - [WithPolishTranslation](#withpolishtranslation) - [WithSpanishTranslation](#withspanishtranslation) - [WithRussianTranslation](#withrussiantranslation) - [WithPortugueseTranslation](#withportuguesetranslation) - [WithGermanTranslation](#withgermantranslation) - [Overriding messages](#overriding-messages) - [Custom translation](#custom-translation) - [Development](#development) - [Build](#build) - [Tests](#tests) - [Benchmarks](#benchmarks) ## Introduction - This documentation is written in short points. - Sometimes a point contains a subpoint. - Occasionally, a point could have a source code following it. - It's for demonstration, and the code is also commented in italic font. - Most code examples in this documentation are using the following set of models: ``` csharp public class BookModel { public string Title { get; set; } public IEnumerable Authors { get; set; } public IEnumerable Languages { get; set; } public int YearOfFirstAnnouncement { get; set; } public int? YearOfPublication { get; set; } public PublisherModel Publisher { get; set; } public bool IsSelfPublished { get; set; } } public class AuthorModel { public string Name { get; set; } public string Email { get; set; } } public class PublisherModel { public string CompanyId { get; set; } public string Name { get; set; } } public enum Language { English, Polish } ``` _Comments are usually placed below the code sample, but that's not the rock-solid principle. The important thing is that they are related to the preceding point, while the next point starts the new thing._ - Vast majority of the code snippets live as functional tests in the [separate project](../tests/Validot.Tests.Functional/). --- ## Specification - Specification is an expression that uses [fluent api](#fluent-api) to describe all conditions of a valid object. - Technically, [specification is a generic delegate](../src/Validot/Specification/Specification.cs), and in most cases, you'll see it in the form of a lambda function. - If you prefer the approach of wrapping validation logic into a separate class, use the [specification holder](#specification-holder). - Specification - considered purely as a C# function - is executed by the [validator](#validator) during its construction (directly or through the [factory](#factory)). - However the validation logic (that specification contains in the form of predicates) is triggered only when [validator](#validator) calls [Validate](#validate) method. - Fluent api consist of commands called in so-called method chain: ``` csharp Specification yearSpecification = m => m .GreaterThan(-10000) .NotEqualTo(0).WithMessage("There is no such year as 0") .LessThan(3000); ``` _Above; four chained commands: `GreaterThan`, `NotEqualTo`, `WithMessage`, `LessThan`. All of them - the entire specification - is the single scope that validates value of type `int`._ - Logically, specification consist of scopes. And the scope could be explained as: - Set of commands that describe validation rules for the same value. - This value is often referred to in this documentation as "scope value". - If the value is null, scope acts according to the [null policy](#null-policy). ``` csharp Specification yearSpecification = s => s .GreaterThan(-10000) .NotEqualTo(0).WithMessage("There is no such year as 0") .LessThan(3000); Specification bookSpecification = s => s .Member(m => m.YearOfFirstAnnouncement, yearSpecification) .Member(m => m.YearOfPublication, m => m .Positive() ) .Rule(m => m.YearOfPublication == m.YearOfFirstAnnouncement).WithMessage("Same year in both places is invalid"); ``` _Above; `yearSpecification` contains four commands in its scope, all validating the value of type `int`._ _Next one, `bookSpecification`, is more complex. Let's analyse it:_ _First [Member](#member) command steps into the `BookModel`'s member of type `int` named `YearOfFirstAnnouncement` and in its scope validates the value using the `yearSpecification` defined earlier._ _Second [Member](#member) command opens scope that validates `YearOfPublication`; this scope contains single rule, `Positive`. Also, according to the [null policy](#null-policy), it requires the nullable member `YearOfPublication` to have a value._ _The last [scope command](#scope-commands), [Rule](#rule) contains a piece of logic for `BookModel` and [parameter command](#parameter-commands) [WithMessage](#withmessage) defines the error message if the predicate fails._ - You can also say that specification is a scope. A "root level" scope. - All commands and their logic are related to a single value (of type `T` in `Specification`). - The [null policy](#null-policy) is followed here as well. - Commands that validate parts of the model are using... specification to describe the scope rules. - Even the root scope behaves as it was placed in [AsModel](#asmodel) command. - There are three types of commands: - [Scope commands](#scope-commands) - contain validation logic and produce [error output](#error-output). - [Parameter commands](#paramter-commands) - changes the behavior of the preceding [scope command](#scope-commands). - [Presence commands](#presence-commands) - sets the scope behavior in case of null value. --- ### Scope commands - Scope command is a command that validates the model by: - executing the validation logic directly: - [Rule](#rule) - executes a custom predicate. - [RuleTemplate](#ruletemplate) and all of the [built-in rules](#rules) - executes a predefined piece of logic. - executing the validation logic wrapped in another [specification](#specification), in the way dependent on the scope value type: - [Member](#member) - executes specification on the model's member. - [AsModel](#asmodel) - executes specification on the model. - [AsCollection](#ascollection) - executes specification on each item of the collection type model. - [AsNullable](#asnullable) - executes specification on the value of the nullable type model. ``` csharp Specification authorSpecification = m => m .Member(m => m.Name, m => m.NotWhiteSpace().MaxLength(100)) .Member(m => m.Email, m => m.Email()) .Rule(m => m.Email != m.Name); ``` _In the above code you can see the specification containing only scope commands._ - Scope command produces [error output](#error-output) if - by any bit of a validation logic - the scope value is considered as invalid. - How is "scope" term related with scope command? - Good to read; [Specification](#specification) - also tries to describe what is a scope. - All scope commands (except for [Rule](#rule) and [RuleTemplate](#ruletemplate)) validate the value by executing a specification (which is a scope). - [Rule](#rule) and [RuleTemplate](#ruletemplate) are slightly different. They contain the most atomic part of validation logic - a predicate. They are still [scope commands](#scope-commands), because: - They determine if the value is valid or not. The only difference is that they execute the logic directly instead of wrapped within another scope. - They produce [error output](#error-output) in case of validation error. --- ### Parameter commands - Parameter command is a command that affects (parametrizes) the closest [scope command](#scope-commands) placed before it. - [WithCondition](#withcondition) - sets execution condition. - [WithPath](#withpath) - sets the path for the [error output](#error-output). - [WithMessage](#withmessage) - overwrites the entire [error output](#error-output) with a single message. - [WithExtraMessage](#withextramessage) - appends a single message to the [error output](#error-output). - [WithCode](#withcode) - overwrites the entire [error output](#error-output) with a single code. - [WithExtraCode](#withextracode) - appends a single code to the [error output](#error-output). - Parameter commands have their order strictly defined and enforced by the language constructs. - So you might notice that some commands are not available from certain places. - Example: [AsNullable](#asnullable) can't be called in the scope that validates `int`. - Example: [WithCode](#withcode) can't be called after [WithMessage](#withmessage), because that doesn't make much sense (double overwrite...). - To know what other commands are allowed to be placed before/after, read the section about the particular command. - It doesn't matter how many parameter commands are defined in the row - they are all related to the closest preceding [scope command](#scope-command) (or [presence command](#presence-commands)). - All the parameter commands start with `With...`, so it's easy to group them visually: ``` csharp Specification authorSpecification = s => s .Member(m => m.Name, m => m.NotWhiteSpace().MaxLength(100)) .WithCondition(m => !string.IsNullOrEmpty(m.Name)) .WithPath("AuthorName") .WithCode("AUTHOR_NAME_ERROR") .Member(m => m.Email, m => m.Email()) .WithMessage("Invalid email!") .WithExtraCode("EMAIL_ERROR") .Rule(m => m.Email != m.Name) .WithCondition(m => m.Email != null && m.Name != null) .WithPath("Email") .WithMessage("Name can't be same as Email"); ``` _Above, you can see that the first [Member](#member) command is configured with the following parameters commands: [WithCondition](#withcondition), [WithPath](#withpath) and [WithCode](#withcode)._ _The second [Member](#member) command is configured with [WithMessage](#withmessage), and [WithExtraCode](#withextracode) commands._ _The third scope command - [Rule](#rule) - is configured with [WithCondition](#withcondition), [WithPath](#withpath), and [WithMessage](#withmessage) commands_ --- ### Presence commands - Presence command is the command that defines the behavior of the entire scope in case of null scope value: - [Required](#required) - scope value must not be null. - if no presence command exists in the scope, this behavior is set implicitly, by default. - [Forbidden](#forbidden) - scope value must be null. - [Optional](#optional) - scope value can be null. - Value gets validated normally if it isn't null, but nothing happens if it is. - Only one presence command is allowed within the scope. - Presence command needs to be the first command in the scope. - Presence commands produce [error output](#error-output) that can be modified with some of the [parameter commands](#parameter-commands). - Not all of them, because e.g. you can't change their [path](#path) or set an [execution condition](#withcondition). - Good to read: [Handling nulls](#null-policy) - details about the null value validation strategy. ``` csharp Specification authorSpecification = m => m .Optional() .Member(m => m.Name, m => m .Optional() .NotWhiteSpace() .MaxLength(100) ) .Member(m => m.Email, m => m .Required().WithMessage("Email is obligatory.") .Email() ) .Rule(m => m.Email != m.Name); ``` _In the example above the entire model is allowed to be null. Similarly - `Name` member. `Email` is required, but the error output will contain a custom message (`Email is obligatory.`) in case of null._ --- ### Error output - Error output is everything that is returned from the scope if - according to the internal logic - the scope value is invalid. - Therefore, the absence of error output means that the value is valid. - Error output can contain: - [Error messages](#message) - human-readable messages explaining what went wrong. - [Error codes](#code) - flags that help to organize the logic around specific errors. - Both. There are no limitations around that. The error output can contain only messages, only codes, or a mix. - The validation process assigns every error output to the [path](#path) where it was produced. - The [path](#path) shows the location where the error occurred. - Sometimes this documentation refers to this action as "saving error output _under the path_" - Good to read: - [Result](#result) - how to get the error output from the validation process. - [Path](#path) - how the paths are constructed. --- #### Message - Messages are primarily targeted to humans. - Use case; logs and the details about invalid models incoming from the frontend. - Use case; rest api returning messages that frontend shows in the pop up. - [Error output](#error-output) can contain one or more error messages. - Good to read: - [Translations](#translations) - how to translate a message or [overwrite](#overriding-messages) the default one. - [Message arguments](#message-arguments) - how to use message arguments. - [MessageMap](#messagemap) - how to read messages from the [validation result](#result). - Message can be set using [WithMessage](#withmessage), [WithExtraMessage](#withmessage), and [RuleTemplate](#ruletemplate) commands. ``` csharp Specification yearSpecification = s => s .Rule(year => year > -300) .WithMessage("Minimum year is 300 B.C.") .WithExtraMessage("Ancient history date is invalid.") .Rule(year => year != 0) .WithMessage("The year 0 is invalid.") .WithExtraMessage("There is no such year as 0.") .Rule(year => year < 10000) .WithMessage("Maximum year is 10000 A.D."); var validator = Validator.Factory.Create(yearSpecification); var result = validator.Validate(-500); result.MessageMap[""][0] // Minimum year is 300 B.C. result.MessageMap[""][1] // Ancient history date is invalid. validator.ToString(); // Minimum year is 300 B.C. // Ancient history date is invalid. ``` _In the above code, [MessageMap](#messagemap) holds the messages assigned to their paths. Empty string as a path means that the error is recorded for the root model._ - Printing returned by [ToString](#tostring) method includes the path before each message. ``` csharp Specification yearSpecification = s => s .Rule(year => year > -300) .WithMessage("Minimum year is 300 B.C.") .WithExtraMessage("Ancient history date is invalid.") .Rule(year => year != 0) .WithMessage("The year 0 is invalid.") .WithExtraMessage("There is no such year as 0.") .Rule(year => year < 10000) .WithMessage("Maximum year is 10000 A.D."); Specification bookSpecification = s => s .Member(m => m.YearOfFirstAnnouncement, yearSpecification) .Member(m => m.YearOfPublication, m => m.AsNullable(yearSpecification)) .Rule(m => m.YearOfFirstAnnouncement <= m.YearOfPublication) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication must be after the year of first announcement"); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { YearOfFirstAnnouncement = 0, YearOfPublication = -100 }; var result = validator.Validate(book); result.MessageMap[""][0]; // Year of publication must be after the year of first announcement result.MessageMap["YearOfFirstAnnouncement"][0]; // "The year 0 is invalid. result.MessageMap["YearOfFirstAnnouncement"][1]; // There is no such year as 0. result.ToString(); // Year of publication must be after the year of first announcement // YearOfFirstAnnouncement: The year 0 is invalid. // YearOfFirstAnnouncement: There is no such year as 0. ``` --- #### Code - Codes are primarily for the parsers and interpreters - they should be short flags, easy to process. - Code cannot contain white space characters. - Good to read: - [CodeMap](#codemap) - how to read codes from the validation result. - [Codes](#codes) - a quick list of all codes from the result. ``` csharp Specification yearSpecification = s => s .Rule(year => year > -300) .WithCode("MAX_YEAR") .Rule(year => year != 0) .WithCode("ZERO_YEAR") .WithExtraCode("INVALID_VALUE") .Rule(year => year < 10000) .WithCode("MIN_YEAR"); var validator = Validator.Factory.Create(yearSpecification); var result = validator.Validate(0); result.Codes; // [ "ZERO_YEAR", "INVALID_VALUE" ] result.CodeMap[""][0]; // [ "ZERO_YEAR" ] result.CodeMap[""][1]; // [ "INVALID_VALUE" ] result.ToString(); // ZERO_YEAR, INVALID_VALUE ``` _In the above example, [CodeMap](#codemap) acts similarly to [MessageMap](#messagemap). Also, for your convenience, [Codes](#codes) holds all the error codes in one place. [ToString()](#tostring) called on the result prints error codes, coma separated, in the first line._ --- ### Path - Path is a string that shows the way of reaching the value that is invalid. - "The way" means which members need to be traversed through in order to reach the particular value. - Example; `Author.Email` path describes the value of `Email` that is inside `Author`. - Path contains segments, and each one stands for one member that the validation context needs to enter in order to reach the value. - Path segments are separated with `.` (dot character). - [Member](#member), which is the way of stepping into the nested level uses the member's name as a segment. ``` csharp model.Member.NestedMember.MoreNestedMember.Email = "invalid_email_value"; var result = validator.Validate(model); result.MessageMap["Member.NestedMember.MoreNestedMember.Email"][0]; // Must be a valid email address result.ToString(); // Member.NestedMember.MoreNestedMember.Email: Must be a valid email address ``` - When it comes to collections (validated with [AsCollection](#ascollection), n-th (counting from zero) item is considered as the member named `#n`. ``` csharp model.MemberCollection[0].NestedMember.MoreNestedMemberCollection[23].Email = "invalid_email_value"; var result = validator.Validate(model); result.MessageMap["MemberCollection[0].NestedMember.MoreNestedMemberCollection[23].Email"][0]; // Must be a valid email address result.ToString(); // MemberCollection[0].NestedMember.MoreNestedMemberCollection[23]: Must be a valid email address ``` _Above, `MemberCollection.#0.NestedMember.MoreNestedMemberCollection.#23.Email:` is the path that leads through 1st item of `MemberCollection` and 24th item of `MoreNestedMemberCollection`._ - You are free to modify the path of every error output using [WithPath](#withpath). --- ### Fluent api - The order the commands in the specification is strictly enforced by the language constructs. Invalid order means compilation error. --- #### Rule - `Rule` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `Rule` defines a single, atomic bit of validation logic with a predicate that accepts the scope value and returns: - `true`, if the scope value is valid. - `false`, if the scope value in invalid. ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m.Rule(isAgeValid); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.IsValid(12); // true ageValidator.IsValid(20); // false ageValidator.Validate(32).ToString(); // Error ``` - If the predicate returns `false`, the `Rule` scope returns [error output](#error-output). - The default error output of `Rule` command is a single [message](#message) key `Global.Error` - Default English translation for it is just `Error`. - It can be altered with [WithMessage](#withmessage) command. ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m.Rule(isAgeValid).WithMessage("The age is invalid"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // The age is invalid ``` _This is just a regular usage of [WithMessage](#withmessage) command that overwrites the entire [error output](#error-output) of the preceding [scope command](#scope-commands) (in this case - `Rule`)._ - `Rule` can be used to validate dependencies between the scope object's members. - If the [error output](#error-output) of such validation should be placed in the member scope rather than its parent, use [WithPath](#withpath) command. ``` csharp Specification bookSpecification = m => m .Rule(book => book.IsSelfPublished == (book.Publisher is null)).WithMessage("Book must have a publisher or be self-published."); var bookValidator = Validator.Factory.Create(bookSpecification); bookValidator.Validate(new BookModel() { IsSelfPublished = true, Publisher = new PublisherModel() }).ToString(); // Book must have a publisher or be self-published. bookValidator.Validate(new BookModel() { IsSelfPublished = true, Publisher = null }).AnyErrors; // false ``` - The value received in the predicate as an argument is never null. - All null-checks on it are redundant, no matter what code analysis has to say about it. - Although the received value is never null, its members could be! ``` csharp Specification publisherSpecification = m => m .Rule(publisher => { if (publisher.Title.Contains(publisher.CompanyId)) { return false; } return true; }); var validator = Validator.Factory.Create(publisherSpecification); validator.Validate(new PublisherModel()); // throws NullReferenceException ``` _In the above example, `publisher` argument is never null, but `Title` and `CompanyId` could be, thus it's high a risk of `NullReferenceException`._ - All unhandled exceptions are bubbled up to the surface and can be caught from `Validate` method. - Exceptions are unmodified and are not wrapped. ``` csharp var verySpecialException = new VerySpecialException(); Specification bookSpecification = m => m.Rule(book => throw verySpecialException); var bookValidator = Validator.Factory.Create(bookSpecification); try { bookValidator.Validate(new BookModel()); } catch(VerySpecialException exception) { object.ReferenceEquals(exception, verySpecialException); // true } ``` - After processing the [Specification](#specification), the [validator](#validator) stores the predicate in its internals. - This is the very reason to be double-cautious when "capturing" variables in the predicate function as you're risking memory leak. Especially when the [validator](#validator) is registered as a singleton in a DI container. --- #### RuleTemplate - `RuleTemplate` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `RuleTemplate` is a special version of [Rule](#rule). - All of the details described in the [Rule](#rule) section also apply to `RuleTemplate`. - The purpose of `RuleTemplate` is to deliver a convenient foundation for predefined, reusable rules. - All [built-in rules](#rules) use `RuleTemplate` under the hood. There are no exceptions, hacks, or special cases. - So if you decide to write your own [custom rules](#custom-rules), you're using the exact same api that the Validot uses. - Technically, there is nothing wrong in placing `RuleTemplate` in the specification directly, but it's not considered as a good practice. - You should rather limit the usage of `RuleTemplate` to its purpose; [custom rules](#custom-rules). - `RuleTemplate` accepts three parameters: - `Predicate` - predicate that tells if the value is valid or not (exactly the same meaning as in [Rule](#rule)). - `message` - error message content. Required. - `args` - a collection of [arguments](#message-arguments) that can be used in the message content. Optional. - `message` sets the single [error message](#message) that will be in the [error output](#error-output) if the predicate returns `false`. - So the result is the same as when using `Rule` followed by `WithMessage`. Below example presents that: ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification1 = m => m.Rule(isAgeValid).WithMessage("The age is invalid"); Specification ageSpecification2 = m => m.RuleTemplate(isAgeValid, "The age is invalid"); var ageValidator1 = Validator.Factory.Create(ageSpecification1); var ageValidator2 = Validator.Factory.Create(ageSpecification2); ageValidator1.Validate(32).ToString(); // The age is invalid ageValidator2.Validate(32).ToString(); // The age is invalid ``` _The above code presents that there is no difference between the basic usage of [Rule](#rule) and [RuleTemplate](#ruletemplate)._ - `args` parameter is optional, and it's a collection of [arguments](#message-arguments) that can be used in placeholders within the error message. - Each argument needs to be created with `Arg` static factory - Ok, technically it doesn't _need_ to be created by the factory, but it's highly recommended as implementing `IArg` yourself could be difficult and more support for it is planned, but not in the very nearly future. - Factory contains helper methods to create arguments related with enums, types, texts, numbers, and guids. - When creating an argument, factory needs: - `name` - needs to be unique across the collection of arguments. - it's the base part of the placeholder: `{name}` - value - value that the message can use - `Arg.Number("minimum", 123)` - creates a number argument named `minimum` with `int` value of `123` - `Arg.Text("title", "Star Wars")` - creates text argument named `title` with `string` value of `"Star Wars"` - Good to read: [Message arguments](#message-arguments) - how to use arguments in messages - Placeholders in the [error message](#message) will be replaced with the value of the related argument. - Name must be the same - Placeholder needs follow the pattern: `{argumentName}` ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate(isAgeValid, "Age must be between {minAge} and {maxAge}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18)); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // Age must be between 0 and 18 ``` - Optionally, placeholders can contain additional parameters: - Good to read: [Message arguments](#message-arguments) ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // Age must be between 0.00 and 18,00 ``` _Notice that the format follows dotnet [custom numeric format strings](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings). The `maxAge` argument also has a different culture set (`pl-PL`, so `,` as a divider instead of `.`)._ - Not all arguments need to be used. - One argument can be used more than once in the same message. - If there is any error (like invalid name of the argument or parameter), no exception is thrown in the code, but the string, unformatted, goes directly to the error output. ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maximumAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // "Age must be between 0.00 and {maximumAge|format=0.00|culture=pl-PL}" ``` _In the above example, `maximumAge` is invalid argument name (`maxAge` would be OK in this case) and therefore - the placeholder stays as it is._ - `RuleTemplate` exposes its arguments to all [messages](#message) in its [error output](#error-output). - Each message can contain only a subset of arguments. - Each message is free to use any formatting it wants. ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ) .WithExtraMessage("Must be more than {minAge}") .WithExtraMessage("Must be below {maxAge|format=0.00}! {maxAge}!"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // Age must be between 0.00 and 18,00 // Must be more than 0 // Must be below 18.00! 18! ``` - Arguments passed to `RuleTemplate` are also available in [WithMessage](#withmessage) and [WithExtraMessage](#withextramessage). ``` csharp Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ) .WithMessage("Only {minAge}-{maxAge}!"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // Only 0-18! ``` - Because all the [built-in rules](#rules) are based on `RuleTemplate`, this is the magic behind altering their error message and still having access to the arguments. ``` csharp Specification ageSpecification = m => m.Between(min: 0, max: 18).WithMessage("Only {min}-{max|format=0.00}!"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString(); // Only 0-18! ``` _In the above example, `Between` is a built-in rule for `int` type values that exposes `min` and `max` parameters to be used in the error messages._ - Good to read: - [Message arguments](#message-arguments) - everything about the available arguments, their types, and parameters. - [Custom rules](#custom-rules) - how to create a custom rule, step by step. - [Rules](#rules) - the detailed list of all arguments available in each of the built-in rule. --- #### Member - `Member` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `Member` executes a specification upon a scope object's member. - `Member` command accepts: - member selector - a lambda expression pointing at a scope object's member. - specification - [specification](#specification) to be executed upon the selected member. - Member selector serves two purposes: - It points at the member that will be validated with the passed [specification](#specification). - So technically it determines type `T` in `Specification` that `Member` accepts as a second parameter. - It defines the nested path under which the entire [error output](#error-output) from the passed [specification](#specification) will be saved. - By default, if the member selector is `m => m.Author`, the [error output](#error-output) will be saved under the path `Author` (as a next segment). ``` csharp Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); var nameValidator = Validator.Factory.Create(nameSpecification); nameValidator.Validate("Adam !!!").ToString(); // Must consist of letters only! // Must not contain whitespace! ``` _In the above example, you can see specification and validation of a string value. Let's use this exact specification inside `Member` command and observe how the entire output is saved under a nested path:_ ``` csharp Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisher = new PublisherModel() { Name = "Adam !!!" }; publisherValidator.Validate(publisher).ToString(); // Name: Must consist of letters only! // Name: Must not contain whitespace! ``` _Let's add one more level:_ ``` csharp Specification bookSpecification = s => s .Member(m => m.Publisher, publisherSpecification); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Publisher = new PublisherModel() { Name = "Adam !!!" } }; authorValidator.Validate(book).ToString(); // Publisher.Name: Must consist of letters only! // Publisher.Name: Must not contain whitespace! ``` - Whether to define a [specification](#specification) upfront and pass it to the `Member` command or define everything inline - it's totally up to you. It doesn't make any difference. - The only thing that is affected is the source code readability. - However, in some particular situations, reusing predefined specifications could lead to having an infinite reference loop in the object. This topic is covered in [Reference loop](#reference-loop) section. ``` csharp Specification bookSpecification = s => s .Member(m => m.Publisher, m => m .Member(m1 => m1.Name, m1 => m1 .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ) ); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Publisher = new PublisherModel() { Name = "Adam !!!" }; }; authorValidator.Validate(book).ToString(); // Publisher.Name: Must consist of letters only! // Publisher.Name: Must not contain whitespace! ``` - Selected member can be only one level from the scope object! - No language construct prevents you from stepping into more nested levels (so no compilation errors), but then, during runtime, [validator](#validator) throws the exception from its constructor (or [factory](#factory)). - This behavior is very likely to be updated in the future versions, so such selectors might be allowed someday... but not now. ``` csharp Specification bookSpecification = s => s .Member(m => m.Publisher.Name, nameSpecification); Validator.Factory.Create(bookSpecification); // throws exception ``` _In the above example, the exception is thrown because member selector goes two levels down (`Publisher.Name`). Please remember that one level down is allowed (just `Publisher` would be totally OK)._ - Selected member can be either property or variable. - It can't be a function. - Type of selected member doesn't matter (can be a reference type, value type, string, enum, or whatever...). - The default path for the [error output](#error-output) (determined by the member selector) can be altered using [WithPath](#withpath) command. - If the selected member contains null, the member scope is still executed and the [error output](#error-output) entirely depends on the [specification](#specification). - It means that null member is not anything special. It's a normal situation, and the behavior relies on the passed [specification](#specification), its [presence commands](#presence-commands), and the [null handling strategy](#null-policy). ``` csharp Specification publisherSpecification = s => s .Member(m => m.Name, m => m .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ); Specification publisherSpecificationRequired = s => s .Member(m => m.Name, m => m .Required().WithMessage("Must be filled in!") .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ); Specification publisherSpecificationOptional = s => s .Member(m => m.Name, m => m .Optional() .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisherValidatorRequired = Validator.Factory.Create(publisherSpecificationRequired); var publisherValidatorOptional = Validator.Factory.Create(publisherSpecificationOptional); var publisher = new PublisherModel() { Name = null }; publisherValidator.Validate(publisher).ToString(); // Name: Required publisherValidatorRequired.Validate(publisher).ToString(); // Name: Must be filled in! publisherValidatorOptional.Validate(publisher).AnyErrors; // false ``` _Without any [presence command](#presence-commands) in `publisherSpecification`, the default behavior is to require the scope value to be non-null. The [error message](#message) can be customized (`publisherSpecificationRequired`) with [Required](#required) command followed by [WithMessage](#withmessage)._ _If the specification starts with `Optional`, no error is returned from the member scope._ --- #### AsModel - `AsModel` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `AsModel` executes a specification upon the scope value. - `AsModel` command accepts only one argument; a specification `Specification`, where `T` is the current scope type. - Technically `AsModel` executes specification in the same scope that it lives itself. - So you can say it's like [Member](#member) command, but it doesn't step into any member. ``` csharp Specification emailSpecification = s => s .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!"); Specification emailAsModelSpecification = s => s .AsModel(emailSpecification); var emailValidator = Validator.Factory.Create(emailSpecification); var emailAsModelValidator = Validator.Factory.Create(emailAsModelSpecification); emailValidator.Validate("invalid email").ToString(); // Must contain @ character! emailAsModelValidator.Validate("invalid email").ToString(); // Must contain @ character! ``` _In the above code you can see that it doesn't matter whether specification is used directly or through `AsModel` - the validation logic is the same and the [error output](#error-output) is saved under the same [path](#path)._ ``` csharp Specification emailSpecification = s => s .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!"); Specification emailNestedAsModelSpecification = s => s .AsModel(s1 => s1 .AsModel(s2 => s2 .AsModel(emailSpecification) ) ); var emailValidator = Validator.Factory.Create(emailSpecification); var emailNestedAsModelValidator = Validator.Factory.Create(emailNestedAsModelSpecification); emailValidator.Validate("invalid email").ToString(); // Must contain @ character! emailAsModelValidator.Validate("invalid email").ToString(); // Must contain @ character! ``` _The above example presents that even several levels of nested `AsModel` commands don't make any difference._ - `AsModel` can be used to execute many independent [specifications](#specification) on the same value. - Effectively, it's like merging [specifications](#specification) into one. ``` csharp Specification atRequiredSpecification = s => s .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!"); Specification allLettersLowerCaseSpecification = s => s .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!"); Specification lengthSpecification = s => s .Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters") .Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters"); Specification emailSpecification = s => s .AsModel(atRequiredSpecification) .AsModel(allLettersLowerCaseSpecification) .AsModel(lengthSpecification); var emailValidator = Validator.Factory.Create(emailSpecification); emailValidator.Validate("Email").ToString(); // Must contain @ character! // All letters need to be lower case! // Must be longer than 5 characters ``` _In the above example, you can see how three separate [specifications](#specification) are - practically - combined into one._ - `AsModel` can be used to mix predefined specifications with inline rules. - Thanks to this, you might "modify" the presence rule in the predefined specification. ``` csharp Specification atRequiredSpecification = s => s .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!"); Specification allLettersLowerCaseSpecification = s => s .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!"); Specification emailSpecification = s => s .Optional() .AsModel(atRequiredSpecification) .AsModel(allLettersLowerCaseSpecification) .Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters") .Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters"); var emailValidator = Validator.Factory.Create(emailSpecification); emailValidator.Validate("Email").ToString(); // Must contain @ character! // All letters need to be lower case! // Must be longer than 5 characters emailValidator.Validate(null).AnyErrors; // false ``` _The example above shows that predefined [specification](#specification) can be expanded with more rules (`AsModel` and subsequent [Rule](#rule) commands)._ _Also, you can observe the interesting behavior that can be described as [presence rule](#presence-commands) alteration. Please notice that `emailSpecification` starts with [Optional](#optional) command that makes the entire model optional (null is allowed) and no error is returned even though both `atRequiredSpecification` and `allLettersLowerCaseSpecification` require model to be not null. Of course, technically it is NOT a modification of their presence settings, but the specification execution would never reach them. Why? The scope value is null, and the scope presence rule `Optional` allows this. And in case of null, as always, no further validation is performed in the scope. Not a big deal, but the example gives an overview of how to play with fluent-api bits to "modify" presence rule._ _Naturally, this works the other way around. Below a short demo of how to make a model required while only using specification that allows the model to be null:_ ``` csharp Specification emailOptionalSpecification = s => s .Optional() .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!"); Specification emailSpecification = s => s .AsModel(emailOptionalSpecification); var emailOptionalValidator = Validator.Factory.Create(emailOptionalSpecification); var emailValidator = Validator.Factory.Create(emailSpecification); emailOptionalValidator.Validate(null).AnyErrors; // false emailOptionalValidator.Validate("Email").ToString(); // Must contain @ character! emailValidator.Validate(null).ToString(); // Required emailValidator.Validate("Email").ToString(); // Must contain @ character! ``` _As you can notice, null passed to `emailOptionalValidator` doesn't produce any validation errors (and it's okay, because the specification allows that with `Optional` command). Having the same specification in `AsModel` effectively changes this behavior. True, null passed to `AsModel` would not return any error output, but null never gets there. The root scope (`emailSpecification`) doesn't allow nulls and it terminates the validation before reaching `AsModel`._ - `AsModel` can be very helpful if you want to bundle many commands and want a single [error message](#message) if any of them indicates validation error. - Saying that, `AsModel` can wrap the entire [specification](#specification) and return single [error message](#message) out of it. - This is just a regular usage of [WithMessage](#withmessage) command and applies to all [scope commands](#scope-commands), not only `AsModel`. It's mentioned here only to present this very specific use case. For more details, please read the [WithMessage](#withmessage) section. ``` csharp Specification emailSpecification = s => s .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!") .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!") .Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters") .Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters"); Specification emailWrapperSpecification = s => s .AsModel(emailSpecification).WithMessage("This value is invalid as email address"); var emailValidator = Validator.Factory.Create(emailSpecification); var emailWrapperValidator = Validator.Factory.Create(emailWrapperSpecification); emailValidator.Validate("Email").ToString(); // Must contain @ character! // All letters need to be lower case! // Must be longer than 5 characters emailWrapperValidator.Validate("Email").ToString(); // This value is invalid as email address ``` _Above, `emailSpecification` contains multiple rules and - similarly - can have several [messages](#message) in its [error output](#error-output). When wrapped within `AsModel` followed by `WithMessage` command, any validation failure results with just a single error message._ _The advantage of this combination is even more visible when you define [specification](#specification) inline and skip all of the error messages attached to the rules - they won't ever be in the output anyway._ ``` csharp Specification emailSpecification = s => s .AsModel(s1 => s1 .Rule(text => text.Contains('@')) .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))) .Rule(text => text.Length > 5) .Rule(text => text.Length < 20) ).WithMessage("This value is invalid as email address"); var emailValidator = Validator.Factory.Create(emailSpecification); emailValidator.Validate("Email").ToString(); // This value is invalid as email address ``` --- #### AsCollection - `AsCollection` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `AsCollection` command has two generic type parameters: `AsCollection`, where: - `TItem` - is a type of the single item in the collection. - `T` - is derived from `IEnumerable`. - `AsCollection` has dedicated versions for some dotnet native collections, so you don't need to specify a pair of `IEnumerable` and `TItem` while dealing with: - `T[]` - `IEnumerable` - `ICollection` - `IReadOnlyCollection` - `IList` - `IReadOnlyList` - `List` - `AsCollection` accepts one parameter; item [specification](#specification) `Specification`. - `AsCollection` executes the passed [specification](#specification) upon each item in the collection. - Internally, getting the items out of the collection is done using `foreach` loop. - Validation doesn't materialize the collection. Elements are picked up using enumerator (as in standard `foreach` loop). - So it might get very tricky when you implement IEnumerable yourself; there is no protection against an infinite stream of objects coming from the enumerator, etc. - Items are validated one after another, sequentially. - Support for async collection validation is coming in the future releases. - [Error output](#error-output) from the n-th item in the collection is saved under the path `#n`. - The counting starts from zero (the first item in the collection is `0` and its [error output](#error-output) will be saved under `#0`). - Validation uses the standard `foreach` loop over the collection, so "n-th item" really means "n-th item received from enumerator". - For some types, the results won't be deterministic, simple because the collection itself doesn't guarantee to keep the order. It might happen that the error output saved under path `#1` next time will be saved under `#13`. This could be a problem for custom collections or some particular use cases, like instance of `HashSet` that gets modified between the two validations. But it will never happen for e.g. array or `List`. ``` csharp Specification evenNumberSpecification = s => s .Rule(number => (number % 2) == 0).WithMessage("Number must be even"); Specification specification = s => s .AsCollection(evenNumberSpecification); var validator = Validator.Factory.Create(specification); var numbers = new[] { 1, 2, 3, 4, 5 }; validator.Validate(numbers).ToString(); // #0: Number must be even // #2: Number must be even // #4: Number must be even ``` _`AsCollection` is able to automatically resolve the type parameters for array. In this case, `AsCollection` is `AsCollection` under the hood._ - `AsCollection` makes sense only if the type validated in the scope is a collection - Well... technically, that's not entirely true, because the only requirement is that it implements `IEnumerable` interface. - Code completion tools (IntelliSense, Omnisharp, etc.) will show `AsCollection` as always available, but once inserted you'll need to define `T` and `TItem`, so effectively - `AsCollection` works only for collections. _Let's consider a custom class holding two collections:_ ``` csharp class NumberCollection : IEnumerable, IEnumerable { public IEnumerable Ints { get; set; } public IEnumerable Doubles { get; set; } IEnumerator IEnumerable.GetEnumerator() => Doubles.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Ints.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); } ``` _You can use `AsCollection` to validate an object as a collection of any type; as long as you are able to specify both generic type parameters:_ ``` csharp Specification evenNumberSpecification = s => s .Rule(number => (number % 2) == 0).WithMessage("Number must be even"); Specification smallDecimalSpecification = s => s .Rule(number => Math.Floor(number) < 0.5).WithMessage("Decimal part must be below 0.5"); Specification specification = s => s .AsCollection(evenNumberSpecification) .AsCollection(smallDecimalSpecification); var validator = Validator.Factory.Create(specification); var numberCollection = new NumberCollection() { Ints = new [] { 1, 2, 3, 4, 5 }, Doubles = new [] { 1.1, 2.8, 3.3, 4.6, 5.9 } } validator.Validate(numberCollection).ToString(); // #0: Number must be even // #1: Decimal part must be below 0.5 // #2: Number must be even // #3: Decimal part must be below 0.5 // #4: Number must be even // #4: Decimal part must be below 0.5 ``` _Above, `AsCollection` command triggers validation of `NumberCollection` as a collection of `int` and `double` items, each with their own [specification](#specification)._ - `AsCollection` doesn't treat the null item as anything special. The behavior is described by the passed [specification](#specification). - `AsCollection` is like [Member](#member) command, but the member selector is pointing at the collection items and the path is dynamic. ``` csharp Specification authorSpecification = s => s .Member(m => m.Email, m => m .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!") ); Specification bookSpecification = s => s .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { null, new AuthorModel() { Email = "foo@bar" }, new AuthorModel() { Email = null }, null, new AuthorModel() { Email = "InvalidEmail" }, null, } }; bookValidator.Validate(book).ToString(); // Authors.#0: Required // Authors.#2.Email: Required // Authors.#3: Required // Authors.#4.Email: Must contain @ character! // Authors.#5: Required ``` _In the code above you can see that null items in the collection result with the default [error message](#message). This is because `authorSpecification` doesn't allow nulls._ _Let's change this and see what happens:_ ``` csharp Specification authorSpecification = s => s .Optional() .Member(m => m.Email, m => m .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!") ); Specification bookSpecification = s => s .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { null, new AuthorModel() { Email = "foo@bar" }, new AuthorModel() { Email = null }, null, new AuthorModel() { Email = "InvalidEmail" }, null, } }; validator.Validate(book).ToString(); // Authors.#2.Email: Required // Authors.#4.Email: Must contain @ character! ``` _Above, `authorSpecification` starts with [Optional](#optional) command, and therefore null items in the collection are allowed._ - `AsCollection` validates the collection items, but the collection itself (as an object) can be normally validated in its own scope normally, as any other value. - One of the widespread use cases is to verify the collection size: ``` csharp Specification authorSpecification = s => s .Optional() .Member(m => m.Email, m => m .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!") ); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification) .Rule(authors => authors.Count() <= 5).WithMessage("Book can have max 5 authors.") ); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { null, new AuthorModel() { Email = "foo@bar" }, new AuthorModel() { Email = null }, null, new AuthorModel() { Email = "InvalidEmail" }, null, } }; bookValidator.Validate(book).ToString(); // Authors.#2.Email: Required // Authors.#4: Must contain @ character! // Authors: Book can have max 5 authors. ``` --- #### AsNullable - `AsNullable` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `AsNullable` "unwraps" the nullable value and provides the way to validate it with a [specification](#specification). - `AsNullable` accepts a single parameter; `Specification`, where `T` is a value type wrapped in `Nullable` (`T?`). - Null value never reaches `AsNullable`, exactly as [handling nulls policy](#null-policy) states. - The passed [specification](#specification) describes `T` that is a value type, so [Optional](#optional) command is not even available. - Null must be handled one level higher (in the [specification](#specification) that contains `AsNullable`). ``` csharp Specification numberSpecification = s => s .Rule(number => number < 10).WithMessage("Number must be less than 10"); Specification nullableSpecification = s => s .AsNullable(numberSpecification); var validator = Validator.Factory.Create(nullableSpecification); validator.Validate(5).AnyErrors; // false validator.Validate(15).ToString(); // Number must be less than 10 validator.Validate(null).ToString(); // Required ``` _In the above code, `Validate` method accepts `int?`. You can observe that the value is unwrapped by `AsNullable` and validated with `numberSpecification` (that describes just `int`)._ _If the nullable value is null, it is stopped at the level of `nullableSpecification`, which doesn't allow nulls. Of course, you can change this behavior:_ ``` csharp Specification numberSpecification = s => s .Rule(number => number < 10).WithMessage("Number must be less than 10"); Specification nullableSpecification = s => s .Optional() .AsNullable(numberSpecification); var validator = Validator.Factory.Create(nullableSpecification); validator.Validate(5).AnyErrors; // false validator.Validate(null).AnyErrors; // false validator.Validate(15).ToString(); // Number must be less than 10 ``` _Now, `nullableSpecification` starts with [Optional](#optional) command, and therefore - null doesn't result with an error. On the other hand - if nullable has a value, it is passed and validated with `numberSpecification`._ - [Every built-in rule](#rules) for a value type has an extra variant for the nullable of this type. - So you don't need to provide `AsNullable` in the most popular and simple cases. ``` csharp Specification numberSpecification = s => s.GreaterThan(0).LessThan(10); Specification nullableSpecification = s => s.GreaterThan(0).LessThan(10); var numberValidator = Validator.Factory.Create(numberSpecification); var nullableValidator = Validator.Factory.Create(nullableSpecification); numberValidator.Validate(5).AnyErrors; // false nullableValidator.Validate(5).AnyErrors; // false numberValidator.Validate(15).ToString(); // Must be less than 10 nullableValidator.Validate(15).ToString(); // Must be less than 10 ``` _In the above code, `GreaterThan` and `LessThan` can be applied to both `Specification` and `Specification`. Technically, they are two separate rules with same names. The consistency of their inner logic is verified by the unit tests._ - `AsNullable` can be handy when you have two versions of the same type (nullable and non-nullable) that can be validated with the same specification. ``` csharp Specification yearSpecification = s => s .Rule(year => year >= -3000).WithMessage("Minimum year is 3000 B.C.") .Rule(year => year <= 3000).WithMessage("Maximum year is 3000 A.D."); Specification bookSpecification = s => s .Member(m => m.YearOfFirstAnnouncement, yearSpecification) .Member(m => m.YearOfPublication, m => m .Optional() .AsNullable(yearSpecification) ); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { YearOfFirstAnnouncement = -4000, YearOfPublication = 4000 }; bookValidator.Validate(book).ToString() // YearOfFirstAnnouncement: Minimum year is 3000 B.C. // YearOfPublication: Maximum year is 3000 A.D. ``` _Above the example how two members - nullable `YearOfPublication` and non-nullable `YearOfFirstAnnouncement` - can be validated with the same specification `yearSpecification`._ --- #### AsConverted - `AsConverted` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `AsConverted` validates the value as a different value. - It could be a value of the same, or of a different type. - The type of the specification is determined by the converter's output. - `AsConverted` accepts: - A conversion function (of type `System.Converter`) that takes the current scope value and outputs the new value. - A specification for type `TOutput` used to validate the converted value. - `AsConverted` executes the delivered specification within the same scope (so all errors are saved on the same level) - So technically, it could be considered as [AsModel](#asmodel), but with a conversion method that's executed upon the scope value before the futher validation. _Below; a snippet presenting how to sanitize the value (for whatever reason that could be an actual case) before validating it with the predefined specification._ ``` csharp Specification nameSpecification = s => s .Rule(name => char.IsUpper(name.First())).WithMessage("Must start with a capital letter!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); Converter sanitizeName = firstName => firstName.Trim(); Specification nameValueSpecification = s => s .AsConverted(sanitizeName, nameSpecification); var nameValidator = Validator.Factory.Create(nameValueSpecification); nameValidator.Validate("Bartosz").AnyErrors; // false nameValidator.Validate(" Bartosz ").AnyErrors; // false nameValidator.Validate(" bartosz ").ToString(); // Must start with a capital letter! nameValidator.Validate(" Bart osz ").ToString(); // Must not contain whitespace! ``` _Of course, type can be different. It's the converter's output that determines the specification. Also, both arguments could be delivered inline:_ ``` csharp Specification authorSpecification = s => s .Member(m => m.Name, m => m.AsConverted( name => name.Length, nameLength => nameLength.Rule(l => l % 2 == 0).WithMessage("Characters amount must be even")) ); var nameValidator = Validator.Factory.Create(authorSpecification); var author = new AuthorModel() { Name = "Bartosz" }; nameValidator.Validate(author).ToString(); // Name: Characters amount must be even ``` - The [template](#template) will contain all errors from the delivered specification, which could lead to misleading case in which the "Required" error is listed as a possible outcome for a value type. - This happens when a value type is converted to a reference type. - If you want to "fix" te template, add [Optional](#optional) at the beginning in the converted value's specification. ``` csharp Specification specification1 = s => s .AsConverted( v => v.ToString(CultureInfo.InvariantCulture), c => c.MaxLength(10).WithMessage("Number must be max 5 digits length") ); Validator.Factory.Create(specification1).Template.ToString(); // Required // Number must be max 5 digits length Specification specification2 = s => s .AsConverted( v => v.ToString(CultureInfo.InvariantCulture), c => c.Optional().MaxLength(10).WithMessage("Number must be max 5 digits length") ); Validator.Factory.Create(specification2).Template.ToString(); // Number must be max 5 digits length ``` --- #### AsType - `AsType` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `AsType` validates the value as if it was of a different type. - If the value can be cast into the target type (using `is`/`as` operators), the validation proceeds with the given specifiction. - If the value can't be cast (`is` check returns false), nothing happens. No error output is recorded and the validation continues with the subsequent commands. - `AsType` accepts: - A specification for type `TTarget` used to validate the cast value. - `AsType` executes the delivered specification within the same scope (so all errors are saved on the same level) - So technically `.AsType(targetTypeSpecification)`, it could be considered as a shortcut for [AsConverted](#asmodel) command combined with [WithCondition](#withcondition): `.AsConverted(v => v as TargetType, targetTypeSpecification).WithCondition(v => v is TargetType)`. _Let's use the classic inheritance example, like: `Animal -> Mammal -> Elephant`_: ``` csharp class Animal { public int AnimalId { get; set; } } class Mammal : Animal { public int MammalId { get; set; } } class Elephant : Mammal { public int ElephantId { get; set; } } ``` _Contructing validator for the class at the bottom of the inheritance graph (`Elephant` in this case), you can use `AsType` and apply specifiction of any of its ancestors_: ``` csharp Specification idSpecification = s => s.NonZero(); Specification animalSpecification = s => s .Member(m => m.AnimalId, idSpecification); Specification elephantSpecification = s => s .Member(m => m.ElephantId, idSpecification) .AsType(animalSpecification); var elephantValidator = Validator.Factory.Create(elephantSpecification); elephantValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 10 }).AnyErrors; // false elephantValidator.Validate(new Elephant() { ElephantId = 0, AnimalId = 10 }).ToString(); // ElephantId: Must not be zero elephantValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 0 }).ToString(); // AnimalId: Must not be zero ``` _It works also in opposite direction. You can create a validator for the ancestor type and use descendants' specifications:_ ``` csharp Specification idSpecification = s => s.NonZero(); Specification elephantSpecification = s => s .Member(m => m.ElephantId, idSpecification); Specification animalSpecification = s => s .Member(m => m.AnimalId, idSpecification) .AsType(elephantSpecification); var animalValidator = Validator.Factory.Create(animalSpecification); animalValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 10 }).AnyErrors; // false animalValidator.Validate(new Elephant() { ElephantId = 0, AnimalId = 10 }).ToString(); // ElephantId: Must not be zero animalValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 0 }).ToString(); // AnimalId: Must not be zero ``` _`AsType` executes only if the type can be cast (`value is TTargetType` is true), so you can use specifiction of unrelated types if for whatever reason you need something that works like a validation hub. Notice that you can construct the specification inline as well (but it's handy to do it with a constructor notation so the compiler can pick up the types from it):_ ``` csharp Specification specification = s => s .AsType(new Specification(number => number.NonZero())) .AsType(new Specification(text => text.NotEmpty())); var validator = Validator.Factory.Create(specification); validator.Validate(12).AnyErrors // false validator.Validate("test").AnyErrors // false validator.Validate(0L).AnyErrors // false, because it's not an integer validator.Validate(0).ToString(); // Must not be zero validator.Validate("").ToString(); // Must not be empty ``` _Naturally, errors from all levels are ultimately grouped by the paths in the report. Below the example of the one containing messages from all three levels:_ ``` csharp Specification idSpecification = s => s.NonZero(); Specification animalSpecification = s => s .Member(m => m.AnimalId, idSpecification); Specification mammalSpecification = s => s .Member(m => m.MammalId, idSpecification) .And() .Member(m => m.AnimalId, idSpecification) .WithMessage("Something wrong with animal from mammal perspective") .And() .AsType(animalSpecification); Specification elephantSpecification = s => s .Member(m => m.ElephantId, idSpecification) .And() .Member(m => m.MammalId, idSpecification) .WithMessage("Something wrong with mammal from elephant perspective") .And() .Member(m => m.AnimalId, idSpecification) .WithMessage("Something wrong with animal from elephant perspective") .And() .AsType(mammalSpecification); var elephantValidator = Validator.Factory.Create(elephantSpecification); elephantValidator.Validate(new Elephant() { ElephantId = 10, MammalId = 10, AnimalId = 10 }).AnyErrors; // false elephantValidator.Validate(new Elephant() { ElephantId = 0, MammalId = 10, AnimalId = 10 }).ToString(); // ElephantId: Must not be zero elephantValidator.Validate(new Elephant() { ElephantId = 10, MammalId = 0, AnimalId = 10 }).ToString(); // MammalId: Must not be zero // MammalId: Something wrong with mammal from elephant perspective elephantValidator.Validate(new Elephant() { ElephantId = 0, MammalId = 0, AnimalId = 0 }).ToString(); // ElephantId: Must not be zero // MammalId: Must not be zero // MammalId: Something wrong with mammal from elephant perspective // AnimalId: Must not be zero // AnimalId: Something wrong with animal from mammal perspective // AnimalId: Something wrong with animal from elephant perspective ``` --- #### AsDictionary - `AsDictionary` is a [scope command](#scope-commands). - Can be placed after: - any command except [Forbidden](#forbidden). - Can be followed by: - any of the [scope commands](#scope-commands). - any of the [parameter commands](#parameter-commands). - `AsDictionary` in its core has three generic type parameters: `AsDictionary`, where: - `T` - is the type of the key-value pair collection (to be exact: `IEnumerable>`), which applies to many dotnet native dictionary-like types. - `TKey` - is the type of the dictionary key. - `TValue` - is the type of the dictionary value. - `AsDictionary` has dedicated versions for some dotnet native dictionary-like types, so you don't need to specify `T`, `TKey`, and `TValue` while dealing with: - `Dictionary` - `IDictionary` - `IReadOnlyDictionary` - `IReadOnlyCollection>` - `AsDictionary` accepts two parameters; - dictionary value [specification](#specification) `Specification`. - dictionary key stringifier `Func keyStringifier` (function that converts `TKey` to a `string` so it could become a part of the [path](#path)). - `AsDictionary` executes the passed [specification](#specification) upon each value in the dictionary. - Internally, getting the items out of the `IEnumerable>` collection is done using `foreach` loop. - Validation doesn't materialize the collection. Elements are picked up using enumerator (as in standard `foreach` loop). - So it might get very tricky when you implement `IEnumerable>` yourself; there is no protection against an infinite stream of objects coming from the enumerator, etc. - For some types, the results won't be deterministic, simple because the collection itself doesn't guarantee to keep the order. It might happen that the error output saved under path `#1` next time will be saved under `#13`. This could be a problem for custom collections or some particular use cases, like instance of `HashSet` that gets modified between the two validations. But it will never happen for e.g. array or `List`. - [Error output](#error-output) from every pair's value is saved under the path of the same pair's stringified key. - After the key is stringified, it gets also also normalized so it could be used as a part of the path: - If the key is null or empty, the path is ` ` (single space). - `<` is trimmed from the beginning of the key and then `.` is trimmed from both ends. - Multiple dots (e.g. `..`, or `.......`) are replaced with a single dot `.`. - Normalization ensures that the error location isn't modified by the key's content. - `AsDictionary` has dedicated version for the dictionaries that have `string` keys, so you don't need to provide the key stringifier. - The dedicated versions are the same as in the regular `AsDictionary`: - `Dictionary` - `IDictionary` - `IReadOnlyDictionary` - `IReadOnlyCollection>` - Essentially, it's like the stringifier is `key => key`. _Let's start with validating an instance of `Dictionary`._ ``` csharp Specification intValueSpecification = s => s.Rule(p => p % 2 == 0).WithMessage("Value must be even"); Specification> specification = s => s.AsDictionary(intValueSpecification); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = 11, ["Two"] = 22, ["Three"] = 33, ["Four"] = 44, ["Five"] = 55 }; validator.Validate(dictionary).ToString(); // One: Value must be even // Three: Value must be even // Five: Value must be even ``` _Above, `intValueSpecification` that specifies integer numbers is used to validate the dictionary values. The dictionary has string keys, and no key stringifier is provided, so the errors are saved under the path determined by the key._ _Below, same example but the key stringifier upper-cases the key._ ``` csharp Specification> specification = s => s .AsDictionary( // AsDictionary's first argument is inline specification for the value s => s.Rule(p => p % 2 == 0).WithMessage("Value must be even"), // AsDictionary's second argument is the key stringifier k => k.ToUpperInvariant() ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = 11, ["Two"] = 22, ["Three"] = 33, ["Four"] = 44, ["Five"] = 55 }; validator.Validate(dictionary).ToString(); // ONE: Value must be even // THREE: Value must be even // FIVE: Value must be even ``` _Null values are handled in the same way as everywhere else: by default, the dictionary value is expected to be non-null._ ``` csharp Specification> specification = s => s .AsDictionary(d => d .Rule(p => p.Length % 2 == 0).WithMessage("Value length must be even") ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = "11", ["Two"] = "22222", ["Three"] = null, ["Four"] = null, ["Five"] = "55" }; validator.Validate(dictionary).ToString(); // Two: Value length must be even // Three: Required // Four: Required ``` _Null as a dictionary value could be enabled by placing `Optional()` as the specification's first command:_ ``` csharp Specification> specification = s => s .AsDictionary(d => d .Optional() .Rule(p => p.Length % 2 == 0).WithMessage("Value length must be even") ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = "11", ["Two"] = "22222", ["Three"] = null, ["Four"] = null, ["Five"] = "55" }; validator.Validate(dictionary).ToString(); // Two: Value length must be even ``` _The path in the template uses `#` as dictionary key, so it's the same behavior as in [AsCollection](#ascollection)._ ``` csharp Specification> specification = s => s .AsDictionary(d => d .Rule(p => p.Length % 2 == 0).WithMessage("Value length must be even") ); var validator = Validator.Factory.Create(specification); validator.Template.ToString(); // Required // #: Required // #: Value length must be even ``` _The keys are normalized, so they can't result in invalid error path or alter it. No multiple dots `...` , no `<` at the beginning._ ``` csharp Specification> specification = s => s .AsDictionary( d => d.Rule(p => p % 2 == 0).WithMessage("Value must be even"), k => k.ToLowerInvariant() ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["OnE..."] = 11, ["ThR...eE"] = 33, ["<<<...FiVe..."] = 55, ["...SeVeN"] = 77, ["<<>`. So it's not a problem to use it with any class that implements this interface:_ ``` csharp class SimpleDictionary : IEnumerable> { public SimpleDictionary(Dictionary items) { Items = items; } private IEnumerable> Items { get; } IEnumerator> IEnumerable>.GetEnumerator() => Items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); } ``` _Above, the custom class that implements `IEnumerable>`. Below, the example presenting how straightforward is to validate it using `AsDictionary`:_ ``` csharp Specification valueSpecification = s => s .Rule(p => p % 2 == 0).WithMessage("Value must be even"); Func keyStringifier = key => { var keyString = ""; for (var i = 0; i < key; i++) { keyString += "X"; } return keyString; }; Specification specification = s => s .AsDictionary(valueSpecification, keyStringifier); var validator = Validator.Factory.Create(specification); var dictionary = new SimpleDictionary(new Dictionary() { [1] = 11, [2] = 22, [3] = 33, [4] = 44, [5] = 55 }); validator.Validate(dictionary).ToString(); // X: Value must be even // XXX: Value must be even // XXXXX: Value must be even ``` _This works also if a class implements multiple generic versions of `IEnumerable>`:_ ``` csharp class DoubleDictionary : IEnumerable>, IEnumerable> { private readonly IEnumerable> _ints; private readonly IEnumerable> _strings; public DoubleDictionary(Dictionary ints, Dictionary strings) { _ints = ints; _strings = strings; } IEnumerator> IEnumerable>.GetEnumerator() => _ints.GetEnumerator(); IEnumerator> IEnumerable>.GetEnumerator() => _strings.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); } ``` _Above, the class that could be validated as dictionary of int keys and values, but also as a dictionary of string keys and values. Below, specification that validates both variants:_ ``` csharp Specification intSpecification = s => s .Rule(p => p % 2 == 0).WithMessage("Value must be even"); Func intKeyStringifier = key => { var keyString = ""; for (var i = 0; i < key; i++) { keyString += "X"; } return keyString; }; Specification stringSpecification = s => s .Rule(p => p.Length < 3).WithMessage("Value must be shorter than 3 characters"); Func stringKeyStringifier = key => key.ToUpperInvariant(); Specification specification = s => s .AsDictionary(intSpecification, intKeyStringifier) .AsDictionary(stringSpecification, stringKeyStringifier); var validator = Validator.Factory.Create(specification); var dictionary = new DoubleDictionary( new Dictionary() { [1] = 11, [2] = 22, [3] = 33, [4] = 44, [5] = 55 }, new Dictionary() { ["One"] = "11", ["Two"] = "222", ["Three"] = "33", ["Four"] = "444", ["Five"] = "555" }); validator.Validate(dictionary).ToString(); // X: Value must be even // XXX: Value must be even // XXXXX: Value must be even // TWO: Value must be shorter than 3 characters // FOUR: Value must be shorter than 3 characters // FIVE: Value must be shorter than 3 characters ``` --- #### WithCondition - `WithCondition` is a [parameter command](#parameter-commands). - Can be placed after: - the related [scope command](#scope-commands). - Can be followed by: - any of the [scope commands](#scope-commands). - other [parameter commands](#parameter-commands): [WithPath](#withpath), [WithMessage](#withmessage), [WithExtraMessage](#withextramessage), [WithCode](#withcode), [WithExtraCode](#withextracode). - `WithCondition` sets the execution condition to the related (preceding) [scope command](#scope-commands). - `WithCondition` accepts single argument; a predicate `Predicate`, where `T` is the current scope type. - So `T` is the same as in `Specification` where the command lives. - The received argument is never null. - If the predicate returns: - `true` - the related [scope command](#scope-commands) is going to be executed. - same behavior as if `When` wasn't there at all. - `false` - the related [scope command](#scope-commands) is skipped. - no validation logic defined in the [scope command](#scope-commands) is triggered. - no [error output](#error-output) is returned. ``` csharp Predicate isValidEmail = email => email.Substring(0, email.IndexOf('@')).All(char.IsLetterOrDigit); Specification emailSpecification = s => s .Rule(isValidEmail) .WithCondition(email => email.Contains('@')) .WithMessage("Email username must contain only letters and digits."); var validator = Validator.Factory.Create(emailSpecification); validator.Validate("John.Doe-at-gmail.com").AnyErrors; // false validator.Validate("John.Doe@gmail.com").ToStringMessages(); // Email username must contain only letters and digits. ``` _Above, the predicate in `WithCondition` checks if the scope value contains `@` character. If true, then the related command scope ([Rule](#rule)) is executed._ _The code shows also that `WithCondition` can makes the code look more clean and readable, as `isValidEmail` predicate doesn't need to contain any logic around `email.IndexOf('@')` returning `-1`. It always has `@` at some position, because otherwise the condition in `WithCondition` prevents the entire `Rule` scope from execution._ - `WithCondition` can be used in pre-verification. - Example; it can ensure that all elements are non-null before validating the relation between them. ``` csharp Predicate isAuthorAPublisher = book => { return book.Authors.Any(a => a.Name == book.Publisher.Name); }; Specification bookSpecification = s => s .Rule(isAuthorAPublisher) .WithCondition(book => book.IsSelfPublished && book.Authors?.Any() == true && book.Publisher?.Name != null ) .WithMessage("Self-published book must have author as a publisher."); var validator = Validator.Factory.Create(bookSpecification); // 1: Condition is met, but the rule fails: var bookModel1 = new BookModel() { IsSelfPublished = true, Authors = new[] { new AuthorModel() { Name = "Bart" } }, Publisher = new PublisherModel() { Name = "Adam" } }; // 2: Condition is met, and the rule doesn't fail: var bookModel2 = new BookModel() { IsSelfPublished = true, Authors = new[] { new AuthorModel() { Name = "Bart" } }, Publisher = new PublisherModel() { Name = "Bart" } }; // 3: Condition is not met: var bookModel3 = new BookModel() { IsSelfPublished = false, Authors = new[] { new AuthorModel() { Name = "Bart" } }, Publisher = null }; validator.Validate(bookModel1).ToString(); // Self-published book must have author as a publisher. validator.Validate(bookModel2).AnyErrors; // false validator.Validate(bookModel3).AnyErrors; // false ``` _Validot never passes null into predicates, but in the above code `isAuthorAPublisher` doesn't care at all about null also at the nested levels (`Publisher` and `Publisher.Name`). The logic in `WithCondition` makes sure that the values are always going to be there._ - `WithCondition` allows you to define many [specifications](#specification) (each validating different case) and execute them selectively, based on some logic. Either exclusively (one at the time) or using any way of mixing them. ``` csharp Specification gmailSpecification = s => s .Rule(email => { var username = email.Substring(0, email.Length - "@gmail.com".Length); return !username.Contains('.'); }).WithMessage("Gmail username must not contain dots."); Specification outlookSpecification = s => s .Rule(email => { var username = email.Substring(0, email.Length - "@outlook.com".Length); return username.All(char.IsLower); }).WithMessage("Outlook username must be all lower case."); Specification emailSpecification = s => s .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!"); Predicate hasGmailAddress = a => a.Email?.EndsWith("@gmail.com") == true; Predicate hasOutlookAddress = a => a.Email?.EndsWith("@outlook.com") == true; Specification authorSpecification = s => s .Member(m => m.Email, gmailSpecification).WithCondition(hasGmailAddress) .Member(m => m.Email, outlookSpecification).WithCondition(hasOutlookAddress) .Member(m => m.Email, emailSpecification) .WithCondition(author => !hasGmailAddress(author) && !hasOutlookAddress(author)); var validator = Validator.Factory.Create(authorSpecification); var outlookAuthor = new AuthorModel() { Email = "John.Doe@outlook.com" }; var gmailAuthor = new AuthorModel() { Email = "John.Doe@gmail.com" }; var author1 = new AuthorModel() { Email = "JohnDoe" }; var author2 = new AuthorModel() { Email = "John.Doe@yahoo.com" }; validator.Validate(outlookAuthor).ToString(); // Email: Outlook username must be all lower case. validator.Validate(gmailAuthor).ToString(); // Email: Gmail username must not contain dots. validator.Validate(author1).ToString(); // Email: Must contain @ character! validator.Validate(author2).AnyErrors; // false ``` _The above code shows how to validate a member with three different specifications, depending on the the email provider._ --- #### WithPath - `WithPath` is a [parameter command](#parameter-commands). - Can be placed after: - the related [scope command](#scope-commands). - other [parameter commands](#parameter-commands): [WithCondition](#withcondition). - Can be followed by: - any of the [scope commands](#scope-commands). - other [parameter commands](#parameter-commands): [WithMessage](#withmessage), [WithExtraMessage](#withextramessage), [WithCode](#withcode), [WithExtraCode](#withextracode). - `WithPath` sets the [path](#path) for the related scope's [error output](#error-output). - `WithPath` accepts one parameter; a path relative to the current scope path. - Example 1; at `FirstLevel.SecondLevel`, setting `ThirdLevel` as path results with `FirstLevel.SecondLevel.ThirdLevel`, not just `ThirdLevel`. - Example 2; at root level, placing setting `Characters` as path results with `Characters`: ``` csharp Specification specification1 = s => s .Rule(email => email.Contains('@')) .WithMessage("Must contain @ character!"); Specification specification2 = s => s .Rule(email => email.Contains('@')) .WithPath("Characters") .WithMessage("Must contain @ character!"); var validator1 = Validator.Factory.Create(specification1); var validator2 = Validator.Factory.Create(specification2); validator1.Validate("invalidemail").ToStringMessages(); // Must contain @ character! validator2.Validate("invalidemail").ToStringMessages(); // Characters: Must contain @ character! ``` _You can observe that the [error output](#error-output) coming from the `Rule` scope command is saved under `Characters` path._ - `WithPath` can move the [error output](#error-output) between levels: - To move it down to the nested level, just use `.` (dot) as a separator, e.g. `FirstLevel.SecondLevel` - Effectively it works like appending the path to the current one. - To move to the upper level, place as many `<` (less-than) as many levels you want to go up - Single `<` works and moves the error output one level up. - Passing `<<<` would move the error output three levels up, etc. - To move it to the upper level, and to the nested level (but e.g. different branch), combine the two methods described above. - Passing `< bookSpecification = s => s .Member(m => m.Publisher, m => m .Member(m1 => m1.Name, m1 => m1 .Rule(name => name.All(char.IsLetter)).WithPath("< specification = s => s .Rule(email => email.Contains('@')) .WithPath("Characters.") .WithMessage("Must contain @ character!"); var validator = Validator.Factory.Create(specification); // throws ArgumentExceptions ``` - `WithPath` is often used to configure [Member](#member) command. - By default, [Member](#member) uses member selector to resolve the next level where the [error output](#error-output) from the passed specification will be saved under. - So if the member selector is `m => m.DescriptionDetails`, then by default the [error output](#error-output) is saved under `DescriptionDetails` - `WithPath` can alter this default value. ``` csharp Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification).WithPath("FirstName"); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisher = new PublisherModel() { Name = "Adam !!!" }; publisherValidator.Validate(publisher).ToString(); // FirstName: Must consist of letters only! // FirstName: Must not contain whitespace! ``` _The default location set by the [Member](#member) command - `Name` - has been changed to `FirstName`._ - `WithPath` can be used to merge error outputs from many scopes into a single path. ``` csharp Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Name must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Name must not contain whitespace!"); Specification companyIdSpecification = s => s .Rule(name => name.Any()).WithMessage("Company Id must not be empty!"); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification).WithPath(" m.CompanyId, companyIdSpecification).WithPath(" nameSpecification = s => s .Rule(name => name.All(char.IsLetter)) .WithPath("Characters") .WithMessage("Must consist of letters only!") .Rule(name => char.IsUpper(name.First())) .WithPath("Grammar") .WithMessage("First letter must be capital!"); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisher = new PublisherModel() { Name = "adam !!!", }; publisherValidator.Validate(publisher).ToString(); // Name.Characters: Must consist of letters only! // Name.Grammar: First letter must be capital! ``` _Above, two rules from the same scope are saving [error messages](#message) into entirely different [paths](#path) (`Characters` and `Grammar`)._ --- #### WithMessage - `WithMessage` is a [parameter command](#parameter-commands). - Can be placed after: - the related [scope command](#scope-commands). - the related [presence commands](#presence-commands): [Required](#required), [Forbidden](#forbidden). - other [parameter commands](#parameter-commands): [WithCondition](#withcondition), [WithPath](#withpath). - Can be followed by: - any of the [scope commands](#scope-commands). - other [parameter commands](#parameter-commands): [WithExtraMessage](#withextramessage), [WithExtraCode](#withextracode). - `WithMessage` overwrites the entire [error output](#error-output) of the related (preceding) [scope command](#scope-commands) with an error output that contains a single [error message](#message). - Effectively it's overwriting all errors with a single message. - `WithMessage` accepts single parameters: message content. - `WithMessage` is the only way to override the default message (`"Error"`) recorded if the predicate in [Rule](#rule) fails: ``` csharp Specification specification = s => s .Rule(year => year != 0); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString(); // Error Specification specificationWithMessage = s => s .Rule(year => year != 0) .WithMessage("Year 0 is invalid"); var validatorWithMessage = Validator.Factory.Create(specificationWithMessage); Validator.Factory.Create(specificationWithMessage).Validate(0).ToString(); // Year 0 is invalid ``` - It doesn't matter how many nested levels or messages/codes the [error output](#error-output) has. If any of the inner validation rules indicates failure, the entire related scope returns a single message passed to `WithMessage`. - If there is no error - there is no [error output](#error-output), and of course, no message as well. ``` csharp Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithMessage("Contains author with invalid email") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; validator.Validate(book).ToString(); // Authors: Contains author with invalid email ``` _Above, [AsCollection](#ascollection) would return messages under multiple different paths. When followed by `WithMessage` even a single error coming from [AsCollection](#ascollection) results with just a single error message._ - When overwriting the [error output](#error-output) of [RuleTemplate](#ruletemplate), `WithMessage` has full access to their [message arguments](#message-arguments) and can use them in its content. - Good to read; - [built-in rules](#rules) - list of the rules and the arguments available - [message args](#message-arguments) - how to use args and placeholders ``` csharp Specification specification = s => s .Between(min: 10, max: 20) .WithMessage("Minimum value is {min}. Maximum value is {max}"); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString(); // Minimum value is 10. Maximum value is 20 ``` _`Between` rule takes two arguments; `max` and `min`. These values can be used within the message - just use the placeholders._ - `WithMessage` combined with [AsModel](#asmodel) can be used to group multiple rules and define one error message for them. - Good to read: [AsModel](#asmodel) - in this section, you can find code example for such a scenario. - Validation result presents messages in: - [ToString](#tostring) - prints messages preceded by their paths, each in a separate line. - [MessageMap](#messagemap) - a dictionary that holds collections of messages grouped by the paths. --- #### WithExtraMessage - `WithExtraMessage` is a [parameter command](#parameter-commands). - Can be placed after: - the related [scope command](#scope-commands). - the related [presence commands](#presence-commands): [Required](#required), [Forbidden](#forbidden). - other [parameter commands](#parameter-commands): [WithCondition](#withcondition), [WithPath](#withpath), [WithMessage](#withmessage). - Can be followed by: - any of the [scope commands](#scope-commands). - other [parameter commands](#parameter-commands): [WithExtraMessage](#withextramessage), [WithExtraCode](#withextracode). - `WithExtraMessage` adds a single [message](#message) to the [error output](#error-output) of the related scope. - `WithExtraMessage` accepts a single parameter: message content. - `WithExtraMessage` is the only way to add additional messages to the [error output](#error-output). - `WithExtraMessage` can be used multiple times, in a row: ``` csharp Specification specification = s => s .Rule(year => year != 0) .WithMessage("Year 0 is invalid") .WithExtraMessage("Year 0 didn't exist") .WithExtraMessage("Please change to 1 B.C. or 1 A.D."); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString(); // Year 0 is invalid // Year 0 didn't exist // Please change to 1 B.C. or 1 A.D. ``` - `WithExtraMessage` acts very similar to [WithMessage](#withmessage), with one important difference; in case of error, it appends message to the [error output](#error-output) of the related scope, instead of overwriting it (as [WithMessage](#withmessage) would do). - Message is added only if the related scope has [error output](#error-output). No error output - no extra message. ``` csharp Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithExtraMessage("Contains author with invalid email") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; validator.Validate(book).ToString(); // Authors.#0.Email: Must be a valid email address // Authors.#1.Email: Must be a valid email address // Authors.#3.Email: Must be a valid email address // Authors: Contains author with invalid email ``` _A similar example to the above one is in the [WithMessage](#withmessage) section. Here, [AsCollection](#ascollection) command returns messages under multiple different paths. When followed by `WithExtraMessage` even a single error coming from [AsCollection](#ascollection) results with an extra message appended to the entire scope._ - When overwriting the [error output](#error-output) of [RuleTemplate](#ruletemplate), `WithMessage` has full access to their [message arguments](#message-arguments) and can use them in its content. - Good to read; - [built-in rules](#rules) - a list of the rules and the arguments available. - [message arguments](#message-arguments) - how to use args and placeholders. ``` csharp Specification specification = s => s .Between(min: 10, max: 20) .WithExtraMessage("Minimum value is {min}. Maximum value is {max}."); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString(); // Must be between 10 and 20 (exclusive) // Minimum value is 10. Maximum value is 20. ``` _`Between` rule takes two arguments; `max` and `min`. These values can be used within the message set with both [WithMessage](#withmessage) and [WithExtraMessage](#withextramessage) - just use the placeholders._ - Validation result presents messages in: - [ToString](#tostring) - prints messages preceded by their paths, each in a separate line. - [MessageMap](#messagemap) - a dictionary that holds collections of messages grouped by the paths. #### WithCode - `WithCode` is a [parameter command](#parameter-commands). - Can be placed after: - the related [scope command](#scope-commands). - the related [presence commands](#presence-commands): [Required](#required), [Forbidden](#forbidden). - other [parameter commands](#parameter-commands): [WithCondition](#withcondition), [WithPath](#withpath). - Can be followed by: - any of the [scope commands](#scope-commands). - other [parameter commands](#parameter-commands): [WithExtraCode](#withextracode). - `WithCode` overwrites the [entire output](#error-output) of the related scope with a single [error code](#code). - `WithCode` accepts one parameter: [code](#code). - [Error code](#code) can't contain white space characters. - `WithCode` acts very similar to [WithMessage](#withmessage), with one important difference; in case of error, it overrides the entire [error output](#error-output) with the [error code](#code), instead of the [error message](#message) (as [WithMessage](#withmessage) would do). - Error code is only if the related scope has any error output. No error output - no error code. - The entire error output is overridden, including the messages! If you want to have both [messages](#message) AND [codes](#code), you should use [WithExtraCode](#withextracode) command. ``` csharp Specification specification = s => s .Rule(year => year != 0) .WithCode("YEAR_ZERO"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(0); result.ToString(); // YEAR_ZERO ``` _Normally, [Rule](#rule) would return error message, but in the above code, the entire error output is replaced with a single code._ - [Validation result](#result) presents codes in: - [Codes](#codes) - a collection of all error codes, from all paths, without duplications. - [ToString()](#tostring) - prints all the codes from [Codes](#codes) collection in the first line, coma separated. - [CodeMap](#codemap) - a dictionary that holds collections of codes grouped by the paths. ``` csharp Specification specification = s => s .AsCollection(m => m .Rule(year => year % 2 == 0).WithCode("IS_EVEN") .Rule(year => year % 2 != 0).WithCode("IS_ODD") ); var validator = Validator.Factory.Create(specification); var result = validator.Validate(new[] { 0, 1, 2, 3, 4 }); result.ToString(); // IS_EVEN, IS_ODD result.Codes; // collection containing two items: // ["IS_EVEN", "IS_ODD"] result.CodeMap["#0"]; // collection with single item: ["IS_EVEN"] result.CodeMap["#1"]; // collection with single item: ["IS_ODD"] result.CodeMap["#2"]; // collection with single item: ["IS_EVEN"] result.CodeMap["#3"]; // collection with single item: ["IS_ODD"] result.CodeMap["#4"]; // collection with single item: ["IS_EVEN"] ``` _In the above example, [ToString](#tostring) prints all [error codes](#code) in the first line. [Codes](#codes) contains all the codes and [CodeMap](#codemap) allows to check exactly where the codes has been recorded._ - `WithCode` can be used to group multiple rules and define one code for any failure among them. ``` csharp Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithCode("INVALID_AUTHORS") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; validator.Validate(book).ToString(); // INVALID_AUTHORS result.Codes; // collection with single item: ["INVALID_AUTHORS"] result.CodeMap["Authors"]; // collection with single item: ["INVALID_AUTHORS"] ``` _Above, [AsCollection](#ascollection) would return messages under multiple different paths. When followed by `WithCode` even a single error coming from [AsCollection](#ascollection) results with just a single error code._ --- #### WithExtraCode - `WithExtraCode` is a [parameter command](#parameter-commands). - Can be placed after: - the related [scope command](#scope-commands). - the related [presence commands](#presence-commands): [Required](#required), [Forbidden](#forbidden). - other [parameter commands](#parameter-commands): [WithCondition](#withcondition), [WithPath](#withpath), [WithMessage](#withmessage), [WithExtraMessage](#withextramessage), [WithCode](#withcode). - Can be followed by: - any of the [scope commands](#scope-commands). - other [parameter commands](#parameter-commands): [WithExtraCode](#withextracode). - `WithExtraCode` adds a single [error code](#code) to the [error output](#error-output) of the related (preceding) [command scope](#scope-commands). - `WithExtraCode` accepts a single parameter; [code](#code). - Reminder; error code can't contain white space characters. - `WithExtraCode` is for [WithCode](#withcode) what [WithExtraMessage](#withextramessage) is for [WithMessage](#withmessage). ``` csharp Specification specification = s => s .Rule(year => year != 0) .WithCode("YEAR_ZERO") .WithExtraCode("INVALID_YEAR"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(0); result.ToString(); // YEAR_ZERO, INVALID_YEAR ``` - `WithExtraCode` acts very similar to [WithCode](#withcode), with one important difference; in case of error it appends the [error code](#code) to the [error output](#error-output) of the related scope, instead of overwriting it (as [WithCode](#withcode) would do). - [Error code](#code) is added only if the related scope has error output. No error output - no extra code. - `WithExtraCode` is the only way to mix error [messages](#message) and [codes](#code) in one [error output](#error-output): ``` csharp Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithExtraCode("INVALID_AUTHORS") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; var result = validator.Validate(book); result.Codes; // collection with single item: ["INVALID_AUTHORS"] result.CodeMap["Authors"]; // collection with single item: ["INVALID_AUTHORS"] result.ToString(); // INVALID_AUTHORS // // Authors.#0.Email: Must be a valid email address // Authors.#1.Email: Must be a valid email address // Authors.#3.Email: Must be a valid email address ``` _In the above example, you can observe how [ToString()][#tostring] prints codes and messages. Of course, both can be detaily examined using [Codes](#codes), [CodeMap](#codemap), and [MessageMap](#messagemap) properties of validation result._ #### Optional - `Optional` is a [presence command](#presence-commands). - Needs to be placed as the first on in the scope. - Can be followed by: - any of the [scope commands](#scope-commands). - `Optional` makes the current scope value optional (null is allowed). - `Optional` is the only way to avoid errors in case of null scope value. ``` csharp Specification specification1 = s => s .Optional() .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator1 = Validator.Factory.Create(specification1); validator1.Validate(null).AnyErrors; // false ``` _Above, `Optional` placed as the first command in the [specification](#specification) makes null a valid case. If we remove it, the null value will result with validation error:_ ``` csharp Specification specification2 = s => s .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator2 = Validator.Factory.Create(specification2); var result2 = validator2.Validate(null); result2.AnyErrors; // true result2.ToString(); // Required ``` _In both cases (with and without `Optional`), when the value is provided - there is no difference in the [error output](#error-output):_ ``` csharp validator1.Validate("a").ToString(); // The minimum length is 3 validator2.Validate("a").ToString(); // The minimum length is 3 validator1.Validate("abc").AnyErrors; // false validator2.Validate("abc").AnyErrors; // false ``` - Using [presence commands](#presence-commands) in the root scope is absolutely correct, but the most common use case for `Optional` is marking members as optional: ``` csharp Specification bookSpecification = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 3).WithMessage("The minimum length is 3") ); var validator = Validator.Factory.Create(bookSpecification); var book1 = new BookModel() { Title = null }; validator.Validate(book1).AnyErrors; // false var book2 = new BookModel() { Title = "a" }; validator.Validate(book2).ToString(); // Title: The minimum length is 3 ``` - Good to read; [null policy](#null-policy) - the entire logic of handling nulls. --- #### Required - `Required` is a [presence command](#presence-commands). - Needs to be placed as the first in the scope. - Can be followed by: - any of the [scope commands](#scope-commands). - [parameter commands](#parameter-commands): [WithMessage](#withmessage), [WithExtraMessage](#withextramessage), [WithCode](#withcode), [WithExtraCode](#withextracode). - `Required` makes the current scope value required (null is not allowed). - Every scope by default requires the incoming value to be non-null, and inserting single `Required` doesn't change anything: ``` csharp Specification specification1 = s => s .Required() .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator1 = Validator.Factory.Create(specification1); var result1 = validator1.Validate(null); result1.AnyErrors; // true result1.ToString(); // Required ``` _Above, `Required` placed as the first command in the specification. If we remove it, literally nothing changes:_ ``` csharp Specification specification2 = s => s .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator2 = Validator.Factory.Create(specification2); var result2 = validator2.Validate(null); result2.AnyErrors; // true result2.ToString(); // Required ``` _Similarly to [Optional](#optional), in both cases (with and without `Required`), when the value is provided - there is no difference ns the [error output](#error-output):_ ``` csharp validator1.Validate("a").ToString(); // The minimum length is 3 validator2.Validate("a").ToString(); // The minimum length is 3 validator1.Validate("abc").AnyErrors; // false validator2.Validate("abc").AnyErrors; // false ``` - `Required` can be used to modify the error output that the scope returns if the scope value is null. - [WithMessage](#withmessage) overrides the default error message. - [WithExtraMessage](#withextramessage) adds the error message to the default one. - [WithCode](#withcode) overrides the default error message with error code. - [WithExtraCode](#withextracode) adds the error code to the default error output. ``` csharp Specification bookSpecification = s => s .Member(m => m.Title, m => m .Required().WithMessage("Title is required").WithExtraCode("MISSING_TITLE") .Rule(title => title.Length > 3).WithMessage("The minimum length is 3") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = null }; var result = validator.Validate(book); result.Codes; // collection with single item: ["MISSING_TITLE"] result.ToString(); // MISSING_TITLE // // Title: Title is required ``` _Above, `Title` member has the default error replaced with message `Title is required` and additional code `MISSING_TITLE`. - Presence errors are special, and you can't move them with [WithPath](#withpath), but there are workarounds: - Check null with [Rule](#rule) command at the upper level and then save the output somewhere else using [WithPath](#withpath). ``` csharp Specification bookSpecification = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 3).WithMessage("The minimum length is 3") ) .Rule(m => m.Title != null) .WithPath("BookTitle") .WithMessage("Title is required") .WithExtraCode("MISSING_TITLE"); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = null }; var result = validator.Validate(book); result.Codes; // [ "MISSING_TITLE" ] result.ToString(); // MISSING_TITLE // // BookTitle: Title is required ``` _Above, `Title` is optional, so no presence error is saved under `Title` path. If `Title` is null, the [error output](#error-output) from [Rule](#rule) is saved under `BookTitle` path._ - Good to read; [null policy](#null-policy) - the entire logic of handling nulls. --- #### Forbidden - `Forbidden` is a [presence command](#presence-commands). - Needs to be placed as the first on in the scope. - Can be followed by: - [parameter commands](#parameter-commands): [WithMessage](#withmessage), [WithExtraMessage](#withextramessage), [WithCode](#withcode), [WithExtraCode](#withextracode). - `Forbidden` makes the current scope forbidden. - Non-null is not allowed, or in other words, the value must be null. - `Forbidden` is exactly opposite to [Required](#required). ``` csharp Specification specification = s => s .Forbidden(); var validator = Validator.Factory.Create(specification); validator.Validate(null).AnyErrors; // false validator.Validate("some value").ToString(); // Forbidden ``` - Similarly to [Required](#required), you can alter the default [error output](#error-output) using parameter commands: ``` csharp Specification bookSpecification = s => s .Member(m => m.Title, m => m .Forbidden().WithMessage("Title will be autogenerated").WithExtraCode("TITLE_EXISTS") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = null }; var result = validator.Validate(book); result.Codes; // [ "TITLE_EXISTS" ] result.ToString(); // TITLE_EXISTS // // Title: Title will be autogenerated ``` - Good to read; [null policy](#null-policy) - the entire logic of handling nulls. --- #### And - `And` contains no validation logic, it's purpose is to visually separate rules in the fluent API method chain. - `And` is a special case - from the technical point of view, `And` could be described as a [scope command](#scope-commands) that doesn't do anything. - The only difference between `And` and a [Rule](#rule) that doesn't do anything are the position restrictions: - `And` can't be placed at the beginning of the specification. - `And` can't be placed at the end of the specification. - `And` helps with automatic formatters that could visually spoil the code: ``` csharp Specification bookSpecificationPlain = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 5) .WithMessage("The minimum length is 5") .Rule(title => title.Length < 10) .WithMessage("The maximum length is 10") ) .Rule(m => !m.Title.Contains("title")) .WithPath("Title") .WithCode("TITLE_IN_TITLE") .Rule(m => m.YearOfFirstAnnouncement < 3000) .WithMessage("Maximum year value is 3000"); ``` _Above, the example of specification where fluent API methods are separated using indentations. Autoformatting (e.g., when pasting this code) could align all methods like this:_ ``` csharp Specification bookSpecificationPlain = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 5).WithMessage("The minimum length is 5") .Rule(title => title.Length < 10).WithMessage("The maximum length is 10") ) .Rule(m => !m.Title.Contains("title")) .WithPath("Title") .WithCode("TITLE_IN_TITLE") .Rule(m => m.YearOfFirstAnnouncement < 3000) .WithMessage("Maximum year value is 3000"); ``` _`And` helps to maintain the readability by visually separating the rules:_ ``` csharp Specification bookSpecificationAnd = s => s .Member(m => m.Title, m => m .Optional() .And() .Rule(title => title.Length > 5).WithMessage("The minimum length is 5") .And() .Rule(title => title.Length < 10).WithMessage("The maximum length is 10") ) .And() .Rule(m => !m.Title.Contains("title")) .WithPath("Title") .WithCode("TITLE_IN_TITLE") .And() .Rule(m => m.YearOfFirstAnnouncement < 3000) .WithMessage("Maximum year value is 3000"); ``` _`And` within the fluent API method chain doesn't affect the logic. Both above specifications always produce equal results_. --- ### Null policy - If the value is entering the scope, presence commands are the first to take action. - If the value entering the scope is null, [scope commands](#scope-commands) are not executed. - It doesn't matter how many rules, commands and logic the scope has - it is skipped, and the validation process leaves the scope. - This is why you don't need to secure your code from `NullReferenceException` in the predicates passed to the [Rule](#rule) (and [RuleTemplate](#ruletemplate)) commands. Validot will never pass null to a predicate. - If the scope doesn't contain any presence command, it acts as it had a single [Required](#required) command at the beginning. - Therefore, every specification by default marks the validated input as required (non-null). - [Required](#required) command itself doesn't do anything extra comparing to the specification without it, however it gives a possibility to change the [error output](#error-output) returned in case the incoming value is null. - By default, the error output contains the single error message key `Global.Required`. - [Optional](#optional) command allows the value to be null. In such a case, validation leaves the scope immediately, and no error output is recorded. - [Forbidden](#forbidden) command requires the value to be null. - By default, the error output contains the single error message key `Global.Forbidden`. - To know how you can modify the error outputs of the presence commands, read their sections: [Required](#required), [Optional](#optional), [Forbidden](#forbidden) ### Reference loop - The reference loop is a loop in a reference graph of your incoming model. - In other words; reference loop exists in a model if you traverse through its members and can reach some reference twice at some point. - On a simple example (imagine a classic linked list, letters represent references): - `A->A`, a direct self-reference; type defines a member of the same type and the object has itself assigned there. - `A->B->C->A`, no direct self-reference, but `A` has member `B`, that has member `C`, that has member `A`, so same reference as at the beginning. ``` csharp public class A { public B B { get; set; } } public class B { public A A { get; set; } } ``` _Above; simple structure `A->B->A`._ - If you're traversing through the object graph and have a reference loop, you can end up in infinite loop and stack overflow exception. - Reference loops are visible in the [Template](#template). - The root of the loop is marked with message key `Global.ReferenceLoop`. - Reference loop is the only case where the [Template](#template) doesn't reflect what ultimately lands in the validation [result](#result). - The validation process inside the loop is running normally. However, the lack of caching might slightly affect performance. ``` csharp Specification specificationB = null; Specification specificationA = s => s .Member(m => m.B, specificationB); specificationB = s => s .Member(m => m.A, specificationA); var validator = Validator.Factory.Create(specificationA); var a = new A() { B = new B() { A = new A() { B = new B() { A = null } } } }; validator.Validate(a).ToString(); // B.A.B.A: Required ``` - Validot has protection against reference loop. - When reference loop is detected in the validated object, `ReferenceLoopException` is thrown from the `Validate` function, with information like: - `Type` - what type was at the beginning of the loop - `Path` - path where the loop starts - `NestedPath` - path where the loop ends (so where the object (of type described in `Type`) has same reference as the object under path visible in `Path`). - `ScopeId` - the id of the scope where the loop happens. This is the information from Validot's internals, not useful in the outside world. However please include it when raising an issue, as it will help the dev team. ``` csharp Specification specificationB = null; Specification specificationA = s => s .Member(m => m.B, specificationB); specificationB = s => s .Member(m => m.A, specificationA); var validator = Validator.Factory.Create(specificationA); var a = new A() { B = new B() { A = new A() { B = new B() { A = null } } } }; a.B.A.B.A = a.B.A; try { validator.Validate(a); } catch(ReferenceLoopException exception) { exception.Path; // "B.A" exception.NestedPath; // "B.A.B.A" exception.Type; // typeof(A) } ``` - Protection against the reference loop is enabled automatically - but only when the risk of such a case is detected. - The protection uses certain resources (validation needs to track the visited references), but performance drop shouldn't be that much noticeable. Please bear that in mind in case you encounter some extreme corner case. - You can explicitly enable/disable the protection in the [settings](#settings). - Please do know what you're doing; e.g. if disabled, there is no protection from stack overflow exception. - There is a risk of reference loop and stack overflow if: - There is a loop in the type graph, and the same types are using the same specification. - It is true that the loop in the type graph indicates possibility of having the loop in the reference graph, but as long as the same types don't use the same specification - it's totally fine because the validation would never end up in the endless loop. - Reference loop is reachable at all. - Validation is based on the specification. If the specification doesn't even step into the members that are in the loop, there is no risk. --- ## Validator - Validator is the object that performs validation process. - Validator validates the object according to the [specification](#specification) `Specification`. - Validator is a generic class `Validator` where `T` is the type of objects it can validate. - Type `T` comes from [specification](#specification) `Specification` - Validator can be created only using can be initialized using the [factory](#factory). - Constructor receives two parameters: - the [specification](#specification). - the [settings](#settings). ``` csharp Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()); var validator = Validator.Factory.Create(specification); ``` _The code above presents that `Validator` can be created with just a [specification](#specification). The code below presents how to apply [settings](#settings) using a fluent api:_ ``` csharp Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()) .And() .Rule(m => m.YearOfPublication > m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue); var validator = Validator.Factory.Create( specification, s => s.WithPolishTranslation() ); ``` - On creation, [factory](#factory) executes the [specification](#specification) function and performs an in-depth analysis of all of the commands that it has. - All of the [error messages](#message) (along with their [translations](#translations)) are pre-generated and cached. - They are exposed in the form of a regular [validation result](#result) ([Template](#template) property). - [Reference loops](#reference-loop) are detected. - If [reference loops](#reference-loop) are possible, [reference loop protection](#withreferenceloopprotection) is automatically enabled, unless you explicitly disable using [WithReferenceLoopProtectionDisabled](#withreferenceloopprotection). - The [reference loop protection](#withreferenceloopprotection) slightly decreases the validation performance. It's because the validator needs to track all visited references in order to prevent stack overflow. - Validation process always executes the commands in the same order as they appear in the specification. - Validation process always executes as few commands as possible in order to satisfy the specification. - Example; if the scope is followed with [WithMessage](#withmessage) or [WithCode](#withcode), internally the validation executes the rules until the first error is found. This is because it doesn't matter how many of the rules inside fails, they're all going to be overridden by [WithMessage](#withmessage) or [WithCode](#withcode). - Example; if the validation process triggered with `failFast` flag, it terminates after detecting the first error. ``` csharp Specification specification = s => s .Member(m => m.Title, m => m .NotEmpty() .NotWhiteSpace() .NotEqualTo("blank") .And() .Rule(t => !t.StartsWith(" ")).WithMessage("Can't start with whitespace") ) .WithMessage("Contains errors!"); var validator = Validator.Factory.Create(specification); var book = new BookModel() { Title = " " }; validator.Validate(book).ToString(); // Title: Contains errors! ``` _Above, the `Title` value is checked by `NotEmpty` and `NotWhiteSpace` rules. `NotWhiteSpace` reports an error, therefore there is no need of executing `NotEqualTo` and `Rule` - as the entire error output is replaced with the message defined in [WithMessage](#withmessage)._ ### Validate - `Validate` is the very function that triggers the full validation process. - It accepts two parameters: - Model of type `T` - the object to validate. - `failFast` (default value: `false`) - a flag indicating whether the process should terminate immediately after detecting the first [error](#error-output). - It returns [validation result](#result). ``` csharp Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()) .And() .Member(m => m.YearOfFirstAnnouncement, m => m.BetweenOrEqualTo(1000, 3000)) .And() .Rule(m => m.YearOfPublication >= m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication needs to be after the first announcement"); var validator = Validator.Factory.Create(specification); var book = new BookModel() { Title = "", YearOfPublication = 600, YearOfFirstAnnouncement = 666 }; var result = validator.Validate(book); result.ToString(); // Title: Must not be empty // YearOfFirstAnnouncement: Must be between 1000 and 3000 (inclusive) // Year of publication needs to be after the first announcement var failFastResult = validator.Validate(book, failFast: true); failFastResult.ToString(); // Title: Must not be empty ``` _In the code above, you can observe that the validation process triggered with `failFast` set to `true` returns only the first [error message](#message) from the regular run. It's always going to be the same message - because validation executes the rules in the same order as they appear in the specification._ ### IsValid - `IsValid` is the highly-optimized version of [Validate](#validate) to check if the model is valid or not. - It's super-fast, but it has its price: no [error output](#error-output) and no [paths](#path). - So you don't know what value is wrong and where it is. - It returns a bool - if `true`, then no error found. Otherwise, `false`. ``` csharp Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()) .And() .Member(m => m.YearOfFirstAnnouncement, m => m.BetweenOrEqualTo(1000, 3000)) .And() .Rule(m => m.YearOfPublication >= m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication needs to be after the first announcement"); var validator = Validator.Factory.Create(specification); var book1 = new BookModel() { Title = "", YearOfPublication = 600, YearOfFirstAnnouncement = 666 }; validator.IsValid(book1); // false var book2 = new BookModel() { Title = "test", YearOfPublication = 1666, YearOfFirstAnnouncement = 1600 }; validator.IsValid(book2); // true ``` - In fact, `IsValid` is so fast that it might be a good idea to call it first and then - if model is invalid - trigger `Validate` to get all of the details. ``` csharp if (!validator.IsValid(heavyModel)) { _logger.Log("Errors found: " + validator.Validate(heavyModel).ToString()); } ``` ### Factory - Factory is the way to create the [validator](#validator) instances. - Factory is exposed through the static member `Factory` of the static class `Validator`: ``` csharp var validator = Validator.Factory.Create(specification); ``` - Factory contains several methods that allows to create [validator](#validator) instances by receiving: - [Specification](#specification) and [settings](#settings) builder - the most popular way - [Validator](#validator) created using this method can validate objects described by the given [specification](#specification), using the [settings](#settings) constructed inline with the fluent API. - [Specification holder](#specification-holder) and [settings](#settings) builder - Similar to the first option, however the [specification](#specification) is acquired from the [specification holder](#specification-holder) - (along with the settings, if it's also a [settings holder](#settings-holder)) - [Specification](#specification) and [settings](#settings) - Similar to the first option, but allows to use [settings](#settings) from somewhere else (e.g. another [validator](#validator)). _Code presenting the usage of [specification holder](#specification-holder) and [validator settings holder](#settings-holder) is placed in their sections._ _Below; simple scenario of creating the [validator](#validator) out the [specification](#specification) and [settings](#settings):_ ``` csharp // specifications: Specification authorSpecification = s => s .Member(m => m.Email, m => m .Email() .And() .EndsWith("@gmail.com") .WithMessage("Only gmail accounts are accepted") ); Specification bookSpecification = s => s .Member(m => m.Title, m => m.NotEmpty().NotWhiteSpace()) .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); // data: var book = new BookModel() { Title = " ", Authors = new[] { new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "john.doe@outlook.com" }, new AuthorModel() { Email = "inv@lidem@il" }, } }; // validator: var validator = Validator.Factory.Create(bookSpecification, s => s .WithTranslation("English", "Texts.Email", "This is not a valid email address!") ); validator.Validate(book).ToString(); // Title: Must not consist only of whitespace characters // Authors.#1.Email: Only gmail accounts are accepted // Authors.#2.Email: This is not a valid email address! // Authors.#2.Email: Only gmail accounts are accepted ``` _Above you can observe that `validator` respects the rules described in the `bookSpecification` as well as the settings (notice the custom error message in `Authors.#2.Email`)._ _Below, let's take a look at the continuation of the previous snippet, showing that we can reuse the settings already built for the other [validator](#validator):_ ``` csharp var validator2 = Validator.Factory.Create(bookSpecification, validator.Settings); validator2.Validate(book).ToString(); // Title: Must not consist only of whitespace characters // Authors.#1.Email: Only gmail accounts are accepted // Authors.#2.Email: This is not a valid email address! // Authors.#2.Email: Only gmail accounts are accepted ``` #### Specification holder - Logically, specification holder is a class that holds [specification](#specification) that [factory](#factory) will fetch and initialize the [validator](#validator) with. - Technically, specification holder is a class that implements `ISpecificationHolder` generic interface. - This interface exposes single member of type `Specification`. ``` csharp interface ISpecificationHolder { Specification Specification { get; } } ``` - [Factory](#factory) has a `Create` method that accepts `ISpecificationHolder` instead of `Specification`. - Specification is taken directly from `Specification` interface member. - Specification holder is a way to wrap the entire specification within a single class. ``` csharp class BookSpecificationHolder : ISpecificationHolder { public BookSpecificationHolder() { Specification titleSpecification = s => s .NotEmpty() .NotWhiteSpace(); Specification emailSpecification = s => s .Email() .EndsWith("@gmail.com").WithMessage("Only gmail accounts are accepted"); Specification authorSpecification = s => s .Member(m => m.Email, emailSpecification); Specification bookSpecification = s => s .Member(m => m.Title, titleSpecification) .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); Specification = bookSpecification; } public Specification Specification { get; } } ``` _Above; example of [specification](#specification) wrapped in the holder. Below; example of usage._ ``` csharp var validator = Validator.Factory.Create(new BookSpecificationHolder()); var book = new BookModel() { Title = " ", Authors = new[] { new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "john.doe@outlook.com" }, new AuthorModel() { Email = "inv@lidem@il" }, } }; validator.Validate(book).ToString(); // Title: Must not consist only of whitespace characters // Authors.#1.Email: Only gmail accounts are accepted // Authors.#2.Email: Must be a valid email address // Authors.#2.Email: Only gmail accounts are accepted ``` #### Settings holder - Logically, a settings holder is a class that holds [settings](#settings) that the [factory](#factory) will fetch and initialize the [validator](#validator) with. - Technically, settings holder is a class that implements `ISettingsHolder`: ``` csharp interface ISettingsHolder { Func Settings { get; } } ``` - Settings holder needs to expose `Settings` member which - practically - is a fluent API builder. Same as the one used in `Validate.Factory.Create` method. - Settings holder is very similar to [specification holder](#specification-holder), but its purpose is to wrap the [settings](#settings). - If the [specification holder](#specification-holder) passed to the [Factory](#factory) implements settings holder as well, the created [validator](#validator) instance will have [settings](#settings) from the holder applied. ``` csharp public class AuthorSpecificationHolder : ISpecificationHolder, ISettingsHolder { public AuthorSpecificationHolder() { Specification emailSpecification = s => s .Email() .EndsWith("@gmail.com"); Specification authorSpecification = s => s .Member(m => m.Email, emailSpecification).WithMessage("Invalid email") .Member(m => m.Name, m => m.NotEmpty()).WithMessage("Name.EmptyValue"); Specification = authorSpecification; Settings = s => s .WithReferenceLoopProtection() .WithPolishTranslation() .WithTranslation(new Dictionary>() { ["English"] = new Dictionary() { ["Name.EmptyValue"] = "Name must not be empty" }, ["Polish"] = new Dictionary() { ["Invalid email"] = "Nieprawidłowy email", ["Name.EmptyValue"] = "Imię nie może być puste" } }); } public Specification Specification { get; } public Func Settings { get; } } ``` _In the above code, [specification](#specification) exposed from the holder internally uses message keys that are resolved in the translations provided in the `Settings` builder. The usage would look like:_ ``` csharp var validator = Validator.Factory.Create(new AuthorSpecificationHolder()); var author = new AuthorModel() { Name = "", Email = "john.doe@outlook.com", }; var result = validator.Validate(author); result.ToString(); // Name: Name must not be empty // Email: Invalid email result.ToString("Polish"); // Name: Imię nie może być puste // Email: Nieprawidłowy email ``` _And the validator's `Settings` proves that settings holder has been used:_ ``` csharp validator.Settings.Translations.Keys // ["English", "Polish"] validator.Settings.Translations["English"]["Name.EmptyValue"] // "Name must not be empty" validator.Settings.Translations["Polish"]["Invalid email"] // "Nieprawidłowy email" validator.Settings.ReferenceLoopProtection // true ``` - The [factory](#factory)'s `Create` method (`Validator.Factory.Create`) that accepts the specification holder, allows to inline modify [settings](#settings) as well. - The inline [settings](#settings) builder overrides the [settings](#settings) from the holder. _Let's see this behavior in the below code:_ ``` csharp var validator = Validator.Factory.Create( new AuthorSpecificationHolder(), s => s .WithReferenceLoopProtectionDisabled() .WithTranslation("English", "Invalid email", "The email address is invalid") ); var author = new AuthorModel() { Name = "", Email = "john.doe@outlook.com", }; validator.Validate(author).ToString(); // Name: Name must not be empty // Email: The email address is invalid validator.Settings.ReferenceLoopProtection; // false ``` #### Reusing settings - Factory can create the [validator](#validator) instance using settings taken from another. - Use the overloaded `Create` method that accepts [specification](#specification) and `IValidatorSettings` instance. - You must pass `IValidatorSettings` instance acquired from a validator. Using custom implementations is not supported and will end up with an exception. _Below, `validator2` uses settings taken from the previously created `validator1`:_ ``` csharp Specification authorSpecification = s => s .Member(m => m.Email, m => m.Email().EndsWith("@gmail.com")) .WithMessage("Invalid email") .And() .Member(m => m.Name, m => m.NotEmpty()) .WithMessage("Name.EmptyValue"); var validator1 = Validator.Factory.Create( authorSpecification, s => s .WithTranslation("English", "Invalid email", "The email address is invalid") .WithTranslation("English", "Name.EmptyValue", "Name must not be empty") ); var validator2 = Validator.Factory.Create(authorSpecification, validator1.Settings); var author = new AuthorModel() { Name = "", Email = "john.doe@outlook.com", }; validator1.Validate(author).ToString() // Name: Name must not be empty // Email: The email address is invalid validator2.Validate(author).ToString() // Name: Name must not be empty // Email: The email address is invalid object.ReferenceEquals(validator1.Settings, validator2.Settings) // true ``` #### Fetching holders - Factory has `FetchHolders` method that scans the provided assemblies for [specification holders](#specification-holder). - You can get all loaded assemblies by calling `AppDomain.CurrentDomain.GetAssemblies()`, or anything else that in your specific case would produce an array of `System.Reflection.Assembly` objects. - You can also be more precise and pick only the desired assemblies. For example, by calling `typeof(TypeInTheAssembly).Assembly`. - [Specification holder](#specification-holder) is included in the result collection if it: - is a class that implements `ISpecificationHolder` interface. - contains a parameterless constructor. - `FetchHolders` returns a collection of `HolderInfo` objects, each containing following members: - `HolderType` - type of the holder, the class that implements `ISpecificationHolder` - `SpecifiedType` - the type that is covered by the [specification](#specification), it's `T` from `ISpecificationHolder` and its member `Specification`. - `HoldsSettings` - a flag, `true` if the class is also a [settings holder](#settings-holder) (implements `ISettingsHolder` interface). - `CreateValidator` - a method that using reflection creates instance of `HolderType` (with its parametless constructor) and then - the validator out of it. - If you want to use it directly, you need to cast it, as the return type is just top-level `object`. - `ValidatorType` - the type of the validator created by `CreateValidator` method. It's always `IValidator` where `T` is `SpecifiedType`. _Let's have a [specification holder](#specification-holder) that holds also the settings:_ ``` csharp public class HolderOfIntSpecificationAndSettings : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; } = s => s .GreaterThanOrEqualTo(1).WithMessage("Min value is 1") .LessThanOrEqualTo(10).WithMessage("Max value is 10"); public Func Settings { get; } = s => s .WithTranslation("English", "Min value is 1", "The minimum value is 1") .WithTranslation("English", "Max value is 10", "The maximum value is 10") .WithTranslation("BinaryEnglish", "Min value is 1", "The minimum value is 0b0001") .WithTranslation("BinaryEnglish", "Max value is 10", "The maximum value is 0b1010") .WithReferenceLoopProtection(); } ``` _It will be detected by `FetchHolders` method:_ ``` csharp var holder = Validator.Factory.FetchHolders(assemblies).Single(h => h.HolderType == typeof(HolderOfIntSpecificationAndSettings)); var validator = (Validator)holder.CreateValidator(); validator.Validate(11).ToString(translationName: "BinaryEnglish"); // The maximum value is 0b1010 ``` _Above, we can observe that the created validator respects the rules and the settings acquired from `HolderOfIntSpecificationAndSettings`._ - `FetchHolders` outputs `HolderInfo` in the following order: - Assemblies are analyzed in the order they are provided. - Or, if called without parameters, it's the order returned by `AppDomain.CurrentDomain.GetAssemblies()`. - For each assembly, holders are analyzed in the order they appear in the output of `assembly.GetTypes()`. - For each [specification holder](#specification-holder), the types are analyzed in the order returned by `type.GetInterfaces()`. #### Dependency injection - Validot doesn't have any dependencies (apart of the pure .NET Standard 2.0), and therefore - there is no direct support for third-party dependency injection containers. - However, the [factory](#factory) is able to [fetch the holders](#fetching-holders) from the referenced assemblies and provides helpers to create [validators](#validator) out of them. - For example, if you want your validators to be automatically registered within the DI container, you can implement the following strategy: - Define [specifications](#specification) for your models in [specification holders](#specification-holder) - Each in a separate class or everything in the single one - it doesn't matter. - Call `Validator.Factory.FetchHolders(AppDomain.CurrentDomain.GetAssemblies())` to get the information about the holders and group the results by the `SpecifiedType`. - instead of `AppDomain.CurrentDomain.GetAssemblies()` you can pass the array of `System.Reflection.Assembly` that the function will scan for `ISpecificationHolder` implementations. - Theoretically, you could define more than one specification for a single type. Let's assume it's not the case here, but as you will notice, the entire operation is merely a short LINQ call. You can easily adjust it to your needs and/or the used DI container's requirements. - Out of every group, take the `ValidatorType` (this is your registered type) and the result of `CreateValidator` (this is your implementation instance). - It's safe to register validators as singletons. _In ASP.NET Core the services registration by default takes place in the ConfigureServices method. Something like `AddValidators` is desirable._ ``` csharp public void ConfigureServices(IServiceCollection services) { // it would be great if this line would scan all referenced projects ... // ... and register validators based on the detected ISpecificationHolder implementations // services.AddValidators(); } ``` _Instead of `AddValidators` you can copy-paste the following lines of code:_ ``` csharp public void ConfigureServices(IServiceCollection services) { // ... registering other dependencies ... // Registering Validot's validators from the current domain's loaded assemblies var holderAssemblies = AppDomain.CurrentDomain.GetAssemblies(); var holders = Validator.Factory.FetchHolders(holderAssemblies) .GroupBy(h => h.SpecifiedType) .Select(s => new { ValidatorType = s.First().ValidatorType, ValidatorInstance = s.First().CreateValidator() }); foreach (var holder in holders) { services.AddSingleton(holder.ValidatorType, holder.ValidatorInstance); } // ... registering other dependencies ... } ``` _You can easily specify the exact assemblies for the Validot to scan (by setting up `holderAssemblies` collection). Validators are created only from the first `ISpecificationHolder` implementation found for each type. To change this logic, adjust the LINQ statement that creates `holders` collection._ _Of course, you can create the fully-featured `AddValidators` extension in the code by saving the following snippet as a new file somewhere in your namespace:_ ``` csharp using System; using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Validot; static class AddValidatorsExtensions { public static IServiceCollection AddValidators(this IServiceCollection @this, params Assembly[] assemblies) { var assembliesToScan = assemblies.Length > 0 ? assemblies : AppDomain.CurrentDomain.GetAssemblies(); var holders = Validator.Factory.FetchHolders(assembliesToScan) .GroupBy(h => h.SpecifiedType) .Select(s => new { ValidatorType = s.First().ValidatorType, ValidatorInstance = s.First().CreateValidator() }); foreach (var holder in holders) { @this.AddSingleton(holder.ValidatorType, holder.ValidatorInstance); } return @this; } } ``` _So it can be used in the ASP.NET Core's `Startup.cs` as below:_ ``` csharp public void ConfigureServices(IServiceCollection services) { // ... registering other dependencies ... services.AddValidators(); // ... registering other dependencies ... } ``` ### Settings - Settings is the object that holds configuration of the validation process that [validator](#validator) will perform: - [Translations](#translations) - values for the message keys used in specification. - [Reference loop](#reference-loop) protection - prevention against stack overflow exception. - Settings are represented by `IValidatorSettings` interface (namespace `Validot.Settings`). - [Validator](#validator) exposes `Settings` property. - `Settings` property is of type `IValidationSettings`, so you can [reuse it](#reusing-settings) in [Factory](#factory) to initialize a new [validator](#validator) instance with the same settings. - All properties in `IValidatorSettings` are read-only, but under the hood there is an instance of `ValidatorSettings` class that has fluent API methods to change the values - You can't create `ValidatorSettings` object directly, but there is no reason to do it. Use the builder pattern exposed by the [factory](#factory). - [Factory](#factory) initializes the settings object with the default values and exposes it through the fluent API: ``` csharp var validator = Validator.Factory.Create(specification, settings => settings .WithReferenceLoopProtection() ); validator.Settings.ReferenceLoopProtectionEnabled; // true ``` #### WithReferenceLoopProtection - `WithReferenceLoopProtection` enables the protection against the [reference loop](#reference-loop). - If not explicitly set, the [validator](#validator) turns it on automatically if the [reference loop](#reference-loop) is theoretically possible according to the [specification](#specification). - `WithReferenceLoopProtectionDisabled` disables the protection against the [reference loop](#reference-loop). - One scenario when this protection is redundant is when you're absolutely sure that the object won't have [reference loops](#reference-loop), because the model is e.g., deserialized from the string. - Settings' property `ReferenceLoopProtectionEnabled` holds to final value. #### WithTranslation - `WithTranslation` accepts three parameters: - `name` - translation name - `messageKey` - message key - `translation` - the content for the given message key ``` csharp settings => settings .WithTranslation("English", "Global.Error", "Error found") .WithTranslation("English", "Global.Required", "Value is required") .WithTranslation("Polish", "Global.Required", "Wartość wymagana"); ``` - Called with keys (`name` or `messageKey`) for the first time, `WithTranslation` creates the underlying dictionaries. - Called multiple times with the same keys (`name` and `messageKey`), `WithTranslation` overwrites the previous value with the provided `translation` value. - `WithTranslation` can also be used to overwrite the existing values (like the default ones or those added before, with another `WithTranslation` method, in whatever form). - In order to overwrite the default value, you need to check the message key that the rule uses. - Good to read; - [Translations](#translations) - how translations work. - [Rules](#rules) - the list of rules and their message keys. ``` csharp Specification specification = s => s .Member(m => m.Email, m => m .NotEmpty() .Email() ) .Member(m => m.Name, m => m .Required().WithMessage("Name is required") ); var author = new AuthorModel() { Email = "" }; var validator1 = Validator.Factory.Create(specification); validator1.Validate(author).ToString(); // Email: Must not be empty // Email: Must be a valid email address // Name: Name is required var validator2 = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "Name is required", "You must fill out the name") .WithTranslation("English", "Texts.NotEmpty", "Text value cannot be empty") ); validator2.Validate(author).ToString(); // Email: Text value cannot be empty // Email: Must be a valid email address // Name: You must fill out the name ``` _In the above code, the default value for `NotEmpty` (message key `Texts.NotEmpty`) has been overridden with the content `Text value cannot be empty`_ - `WithTranslation` has a version (via extension method) that wraps the base method and accepts: - `name` - translation name - `translation` - dictionary; its keys are set as `messageKey` and the related values as `translations`. ``` csharp settings => settings .WithTranslation("English", new Dictionary() { ["Global.Error"] = "Error found", ["Global.Required"] = "Value is required", }) .WithTranslation("Polish", new Dictionary() { ["Global.Required"] = "Wartość wymagana", }); ``` - `WithTranslation` has a version (via extension method) that wraps the base method and accepts `IReadOnlyDictionary>`: - the keys is passed as `name` - the value is another dictionary; its keys are set as `messageKey` and the related values as `translations`. ``` csharp settings => settings .WithTranslation(new Dictionary>() { ["English"] = new Dictionary() { ["Global.Error"] = "Error found", ["Global.Required"] = "Value is required", }, ["Polish"] = new Dictionary() { ["Global.Required"] = "Wartość wymagana", } }); ``` - `WithTranslation` also has extension methods that wrap the base method and add entries for a specific translation: - `WithEnglishTranslation` - adds [English translation](../src/Validot/Translations/English/EnglishTranslation.cs), by default always present in the settings. - `WithPolishTranslation` - adds [Polish translation](../src/Validot/Translations/Polish/PolishTranslation.cs), by default, always present in the settings. ### Template - `Template` is a byproduct of the analysis that the [validator](#validator) performs during the initialization. - Validator traverses through all of the commands in [specification](#specification), determines and caches all the possible [paths](#path), [messages](#message) and [codes](#code). - `Template` is the object of the same type as [results](#result) (`IValidationResult`), so you can check all of the cached data with the same properties, verify the translations, error codes, etc. ``` csharp Specification specification = s => s .NotEmpty() .NotWhiteSpace().WithMessage("White space is not allowed") .Rule(m => m.Contains('@')).WithMessage("Must contain @ character"); var validator = Validator.Factory.Create(specification); validator.Template.ToString(); // Required // Must not be empty // White space is not allowed // Must contain @ character ``` - The first difference between the actual [validation result](#result) and the `Template` is that the `Template` doesn't have indexes in the [paths](#path). - It doesn't make any sense because `Template` isn't related to any particular object. - Example; `Collection.#.NestedCollection.#.Something` instead of `Collection.#5.NestedCollection.#0.Something` that would appear in the [result](#result) of the [Validate](#validate) method. ``` csharp Specification specification = s => s .Member(m => m.Authors, m => m .AsCollection(m1 => m1 .Member(m2 => m2.Name, m2 => m2.NotEmpty()) ) ); var validator = Validator.Factory.Create(specification); validator.Template.ToString(); // Required // Authors: Required // Authors.#: Required // Authors.#.Name: Required // Authors.#.Name: Must not be empty ``` - The second difference between the actual [validation result](#result) and the `Template` is that the in case of the [reference loop](#reference-loop), `Template` contains only the message set by the key `Global.ReferenceLoop`. - The default English translation is `(reference loop)`. - Such error output is placed at the root of the reference loop. ``` csharp Specification specificationB = null; Specification specificationA = s => s .Member(m => m.B, specificationB); specificationB = s => s .Member(m => m.A, specificationA); var validator = Validator.Factory.Create(specificationA); validator.Template.ToString(); // Required // B: Required // B.A: (reference loop) ``` - `Template` contains all theoretically possible errors, so it would also have the [error outputs](#error-output) that in the real world would be exclusive to each other (literally all predicates are ignored). - It also means, that the printing of the `Template` (generated by [ToString](#tostring) method) could be quite large. ``` csharp Specification authorSpecification = s => s .Member(m => m.Email, m => m .NotWhiteSpace().WithMessage("Email cannot be whitespace") .Email() ) .Member(m => m.Name, m => m .NotEmpty() .NotWhiteSpace() .MinLength(2) ); Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()).WithExtraCode("EMPTY_TITLE") .Member(m => m.YearOfFirstAnnouncement, m => m.BetweenOrEqualTo(1000, 3000)) .Member(m => m.Authors, m => m .AsCollection(authorSpecification) .MaxCollectionSize(4).WithMessage("Book shouldn't have more than 4 authors").WithExtraCode("MANY_AUTHORS") ) .Rule(m => m.YearOfPublication >= m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication needs to be after the first announcement"); var validator = new Validator(specification); validator.Template.ToString(); // EMPTY_TITLE, MANY_AUTHORS // // Required // Year of publication needs to be after the first announcement // Title: Required // Title: Must not be empty // YearOfFirstAnnouncement: Must be between 1000 and 3000 (inclusive) // Authors: Required // Authors: Book shouldn't have more than 4 authors // Authors.#: Required // Authors.#.Email: Required // Authors.#.Email: Email cannot be whitespace // Authors.#.Email: Must be a valid email address // Authors.#.Name: Required // Authors.#.Name: Must not be empty // Authors.#.Name: Must not consist only of whitespace characters // Authors.#.Name: Must be at least 2 characters in length ``` ## Result - Validation result is an object of type `IValidationResult` and is produced by the [Validate](#validate) method. - The result is internally linked with the [validator](#validator) that created it. - This is the reason behind its ability to translate the [messages](#message) that are registered within the [validator](#validator). - This is also the reason you shouldn't store the `IValidationResult` object for too long or pass it around your system. - However, you can retrieve the data using its properties (listed below, here in this section). They are safe to operate on. ### AnyErrors - `AnyErrors` is the flag that returns: - `true` - if there are errors. - `false` - no errors and the object is valid according to the specification. ``` csharp Specification specification = s => s .NotEmpty(); var validator = Validator.Factory.Create(specification); var result1 = validator.Validate("test"); result1.AnyErrors; // false var result2 = validator.Validate(""); result2.AnyErrors; // true ``` ### Paths - `Paths` property is the collection of all [paths](#path) that contain [error output](#error-output). - It doesn't matter whether it's an [error output](#error-output) with only [messages](#message), [codes](#code), or a mix. - `Paths` can be used to check if the value under a certain [path](#path) is valid or not. - `Paths` collection doesn't contain duplicates. - To check what [messages](#message) and/or [codes](#code) have been saved under a [path](#path), you need to use [CodeMap](#codemap) and [MessageMap](#messagemap). - The order of the elements in the collection is not guaranteed. - The empty string means the root model. ``` csharp Specification authorSpecification = s => s .Member(m => m.Email, m => m.Email().WithCode("EMAIL")) .Member(m => m.Name, m => m .NotEmpty() .MinLength(3) .NotContains("X").WithMessage("X character is not allowed in name") ); Specification bookSpecification = s => s .Member(m => m.Title, m => m.NotWhiteSpace()) .Member(m => m.Authors, m => m .AsCollection(authorSpecification) ) .Rule(m => m.IsSelfPublished == false).WithCode("ERROR_SELF_PUBLISHED"); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = "", Authors = new[] { new AuthorModel() { Email = "john.doe@gmail.com", Name = "X" }, new AuthorModel() { Email = "jane.doe@gmail.com", Name = "Jane" }, new AuthorModel() { Email = "inv@lidem@il", Name = "Jane" } }, IsSelfPublished = true }; var result = bookValidator.Validate(book); result.Paths; // [ "", "Title", "Authors.#0.Name", "Authors.#2.Email" ] ``` _In the above example, all [paths](#path) with errors are listed in `Paths` collection. Including `Email` and root that contain a single [error code](#code). Also, `Authors.#0.Name` path has two errors (from `MinLength` and `NotContains` commands), but it's present only once._ ### Codes - `Codes` property is the collection of all the [codes](#code) in the [error output](#error-output). - The path doesn't matter. All codes from all the error outputs are listed. - `Codes` collection can be used to check if some [code](#code) has been recorded for the validated model. - To check where exactly, you need to use [CodeMap](#codemap). - `Codes` collection doesn't contain duplicates. - The order of the elements in the collection is not guaranteed. ``` csharp Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("NAME_ERROR") .MinLength(3).WithCode("SHORT_FIELD").WithExtraCode("NAME_ERROR") ) .Member(m => m.CompanyId, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("COMPANYID_ERROR") .NotContains("ID").WithCode("ID_IN_CONTENT") ) .Rule(m => m.Name != m.CompanyId).WithCode("SAME_VALUES"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "" }; var result = validator.Validate(publisher); result.Codes; // [ "EMPTY_FIELD", "NAME_ERROR", "SHORT_FIELD", "COMPANYID_ERROR", "SAME_VALUES" ] ``` _In the above code, `EMPTY_FIELD` and `NAME_ERROR` are not duplicated in `Codes`, despite the fact that several different rules save them in the [error output](#error-output)._ ### CodeMap - `CodeMap` is a dictionary that links [error codes](#code) with their [paths](#path). - `CodeMap` is property of type `IReadOnlyDictionary>`, where: - the key is the [path](#path). - the value is the list of error [codes](#code) saved under the related path. - the list can contain duplicates. ``` csharp Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("NAME_ERROR") .MinLength(3).WithCode("SHORT_FIELD").WithExtraCode("NAME_ERROR") ) .Member(m => m.CompanyId, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("COMPANYID_ERROR") .NotContains("company").WithCode("COPANY_IN_CONTENT") .NotContains("id").WithMessage("Invalid company value") ) .Rule(m => m.Name is null || m.CompanyId is null).WithCode("NULL_MEMBER"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "some_id" }; var result = validator.Validate(publisher); result.CodeMap["Name"]; // [ "EMPTY_FIELD", "NAME_ERROR", "SHORT_FIELD", "NAME_ERROR" ] result.CodeMap[""]; // [ "NULL_MEMBER" ] ``` - If the [path](#path) is not present in `CodeMap.Keys` collection, it means no code has been saved for it. - If the path present in [Paths](#paths) collection is missing in `CodeMap.Keys`, it means that the [error output](#error-output) for it doesn't contain codes. You should check [MessageMap](#messagemap) instead. ``` csharp result.Paths.Contains("CompanyId"); // true result.CodeMap.Keys.Contains("CompanyId"); // false result.MessageMap.Keys.Contains("CompanyId"); // true ``` ### MessageMap - `MessageMap` is a dictionary that links [error messages](#message) with their [paths](#path). - `MessageMap` is property of type `IReadOnlyDictionary>`, where: - the key is the [path](#path). - the value is the list of [error messages](#message) saved under the related [path](#path). - the list can contain duplicates. - `MessagesMap` always uses the default translation (`English`). - If you want to have them translated with a different dictionary, use [GetTranslatedMessageMap](#gettranslatedmessagemap) function. - Good to read; [Translations](#translations). ``` csharp Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty().WithMessage("The field is empty").WithExtraMessage("Error in Name field") .MinLength(3).WithMessage("The field is too short").WithExtraMessage("Error in Name field") ) .Member(m => m.CompanyId, m => m .NotEmpty().WithMessage("The field is empty").WithExtraMessage("Error in CompanyId field") .NotContains("company").WithMessage("Company Id cannot contain 'company' word") .NotContains("id").WithCode("ID_IN_COMPANY") ) .Rule(m => m.Name is null || m.CompanyId is null) .WithMessage("All members must be present"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "some_id" }; var result = validator.Validate(publisher); result.MessageMap["Name"]; // [ "The field is empty", "Error in Name field", "The field is too short", "Error in Name field" ] result.MessageMap[""]; // [ "All members must be present" ] ``` - If the [path](#path) is not present in `MessagesMap.Keys` collection, it means no [code](#code) has been saved for it. - If the path present in [Paths](#paths) collection is missing in `MessageMap.Keys`, it means that the [error output](#error-output) for it doesn't contain codes. You should check [CodeMap](#codemap) instead. ``` csharp result.Paths.Contains("CompanyId"); // true result.MessageMap.Keys.Contains("CompanyId"); // false result.CodeMap.Keys.Contains("CompanyId"); // true ``` ### GetTranslatedMessageMap - `GetTranslatedMessageMap` returns similar result to `MessageMap`. - Structure and meaning are the same but the messages are translated. - `GetTranslatedMessageMap` accepts single parameter; `translationName`: ``` csharp Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .MinLength(3).WithMessage("Name is too short") ) .Member(m => m.Email, m => m .Email() ); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() .WithTranslation("Polish", "Name is too short", "Imię jest zbyt krótkie") ); var author = new AuthorModel() { Name = "", Email = "inv@lidem@il" }; var result = validator.Validate(author); var englishMessageMap = result.GetTranslatedMessageMap("English"); englishMessageMap["Name"]; // [ "Must not be empty", "Name is too short" ] englishMessageMap["Email"]; // [ "Must be a valid email address" ] var polishMessageMap = result.GetTranslatedMessageMap("Polish"); polishMessageMap["Name"]; // [ "Musi nie być puste", "Imię jest zbyt krótkie" ] polishMessageMap["Email"]; // [ "Musi być poprawnym adresem email" ] ``` - If the given `translationName` is not present in [TranslationNames](#translationnames) list, exception is thrown. - Good to read; - [Translations](#translations) - how translation works - [WithTranslation](#withtranslation) - how to set translation messages ``` csharp var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() ); var result = validator.Validate(author); result.GetTranslatedMessageMap("Russian"); // throws KeyNotFoundException ``` ### TranslationNames - `TranslationNames` is a list of all translation names that can be used to translate [messages](#message) in the [result](#result). - The messages can be translated with [ToString](#tostring) and [GetTranslatedMessageMap](#gettranslatedmessagemap) functions. ``` csharp var validator = Validator.Factory.Create(specification); var result = validator.Validate(model); result.TranslationNames; // [ "English" ] ``` - The list is the same as in the [Validator](#validator) that produced the result. ``` csharp var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() ); var result = validator.Validate(model); result.TranslationNames; // [ "Polish", "English" ] ``` ### ToString - `ToString` is a helper method that prints the error [codes](#code) and [messages](#message) in the following format: - In the first line: all the [codes](#code) from [Codes](#codes) collection, comma separated. - If no [error codes](#code), the printing starts directly with the [messages](#message). - If there is a line with error codes, it's separated from the messages with the empty line. - Each message is printed in a separate line, each one preceded with its [path](#path). - In the root path, the message starts from the beginning of the line. - Order of the codes and messages are is guaranteed. ``` CODE1, CODE2, CODE3 Root message Path: Message in the path Path.Nested: Nested message 1 Path.Nested: Nested message 2 Path.Nested: Nested message 3 ``` - Effectively, it's like printing [Codes](#codes) in the first line and then [MessageMap](#messagemap). - The basic version of `ToString` always uses the default translation, which is `English`. ``` csharp Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .WithMessage("The field is empty") .WithExtraMessage("Error in Name field") .WithExtraCode("NAME_EMPTY") .MinLength(3) .WithMessage("The field is too short") .WithExtraCode("NAME_TOO_SHORT") ) .Member(m => m.CompanyId, m => m .NotEmpty() .NotContains("id") .WithCode("ID_IN_COMPANY") ) .Rule(m => m.Name is null || m.CompanyId is null) .WithMessage("All members must be present"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "some_id" }; var result = validator.Validate(publisher); result.ToString(); // NAME_EMPTY, NAME_TOO_SHORT, ID_IN_COMPANY // Name: The field is empty // Name: Error in Name field // Name: The field is too short // All members must be present ``` - `ToString` also has a version that accepts a single parameter; `translationName`. Use to retrieve the same content, but translated using the dictionary of the given name. - `translationName` needs to be listed in [TranslationNames](#translationnames). Otherwise, you can expect an exception. ``` csharp Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .MinLength(3) ) .Member(m => m.CompanyId, m => m .NotEmpty().WithMessage("CompanyId field is required") ); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() .WithTranslation("Polish", "CompanyId field is required", "Pole CompanyId jest wymagane") ); var publisher = new PublisherModel() { Name = "", CompanyId = "" }; var result = validator.Validate(publisher); result.ToString(); // Name: Must not be empty // Name: Must be at least 3 characters in length // CompanyId: CompanyId field is required result.ToString("Polish"); // Name: Musi nie być puste // Name: Musi być długości minimalnie 3 znaków // CompanyId: Pole CompanyId jest wymagane result.ToString("Russian"); // throws exception ``` - Good to read; - [Translations](#translations) - how translation works. - [WithTranslation](#withtranslation) - how to set translation messages. - In case of a valid result, `ToString` prints simple message: `OK`: ``` csharp Specification specification = s => s; var validator = Validator.Factory.Create(specification); var model = new PublisherModel(); var result = validator.Validate(model); result.AnyErrors; // false result.ToString(); // OK ``` ## Rules ### Global rules | Fluent api | Message key | Args | | - | - | - | | `Rule` | `Global.Error` | - | | `Required` | `Global.Required` | - | | `Forbidden` | `Global.Forbidden` | - | | reference loop | `Global.ReferenceLoop` | - | - [Reference loop](#reference-loop) error is a special case, it doesn't have the dedicated fluent api command and is related to the existence of [reference loop](#reference-loop). ### Bool rules - Rules apply to `bool`. | Fluent api | Message key | Args | | - | - | - | | `True` | `BoolType.True` | - | | `False` | `BoolType.False` | - | ### Char rules - Rules apply to `char`. - `char` can be validated by the below rules and all of the [number rules](#number-rules) for the unsigned types. | Fluent api | Message key | Args | | - | - | - | | `EqualToIgnoreCase` | `CharType.True` | `value` : [text](#text-argument) | | `NotEqualToIgnoreCase` | `CharType.False` | `value` : [text](#text-argument) | ### Collections rules - Rules apply to any object that implements `IEnumerable`. - There are dedicated generic versions for: `T[]`, `IEnumerable`, `IList`, `IReadOnlyCollection`, `IReadOnlyList`, `List`. - Dedicated means that you don't need to specify `IEnumerable` and `T` explicitly as generic parameters. | Fluent api | Message key | Args | | - | - | - | | `EmptyCollection` | `Collections.EmptyCollection` | - | | `NotEmptyCollection` | `Collections.EmptyCollection` | - | | `ExactCollectionSize` | `Collections.ExactCollectionSize` | `size` : [number](#number-argument) | | `MaxCollectionSize` | `Collections.MaxCollectionSize` | `max` : [number](#number-argument) | | `MinCollectionSize` | `Collections.MinCollectionSize` | `min` : [number](#number-argument) | | `CollectionSizeBetween` | `Collections.CollectionSizeBetween` | `min` : [number](#number-argument),
`max` : [number](#number-argument) | ### Numbers rules - Rules for all unsigned and signed types: | Fluent api | Message key | Args | | - | - | - | | `EqualTo` | `Numbers.EqualTo` | `value` : [number](#number-argument) | | `NotEqualTo` | `Numbers.EqualTo` | `value` : [number](#number-argument) | | `GreaterThan` | `Numbers.Greater` | `min` : [number](#number-argument) | | `GreaterThanOrEqualTo` | `Numbers.GreaterThanOrEqualTo` | `min` : [number](#number-argument) | | `LessThan` | `Numbers.LessThan` | `max` : [number](#number-argument) | | `LessThanOrEqualTo` | `Numbers.LessThanOrEqualTo` | `max` : [number](#number-argument) | | `Between` | `Numbers.LessThan` | `min` : [number](#number-argument),
`max` : [number](#number-argument) | | `BetweenOrEqualTo` | `Numbers.LessThanOrEqualTo` | `min` : [number](#number-argument),
`max` : [number](#number-argument) | | `NonZero` | `Numbers.NonZero` | - | | `Positive` | `Numbers.Positive` | - | | `NonPositive` | `Numbers.NonPositive` | - | - Extra rules just for signed types: | Fluent api | Message key | Args | | - | - | - | | `Negative` | `Numbers.Negative` | - | | `NonNegative` | `Numbers.NonNegative` | - | - Floating-point types `double` and `float` have a special version of some rules that allows to set the tolerance level - the default value of `tolerance` is `0.0000001`. - this is pretty much enforced by the specifics of the binary system, so if you want to avoid the risk, please use `decimal` type. | Fluent api | Message key | Args | | - | - | - | | `EqualTo` | `Numbers.EqualTo` | `value` : [number](#number-argument), `tolerance` : [number](#number-argument) | | `NotEqualTo` | `Numbers.EqualTo` | `value` : [number](#number-argument), `tolerance` : [number](#number-argument) | | `NonZero` | `Numbers.NonZero` | `tolerance` : [number](#number-argument) | | `NonNan` | `Numbers.NonNan` | - | ### Texts rules - Content rules - The enum that sets the comparison strategy is the standard [StringComparison](https://docs.microsoft.com/en-us/dotnet/api/system.stringcomparison?view=netstandard-2.0) enum. - The default value of `stringComparison` is `Ordinal`. | Fluent api | Message key | Args | | - | - | - | | `EqualTo` | `Texts.EqualTo` | `value` : [text](#text-argument),
`stringComparison` : [enum](#enum-argument) | | `NotEqualTo` | `Texts.NotEqualTo` | `value` : [text](#text-argument),
`stringComparison` : [enum](#enum-argument) | | `Contains` | `Texts.Contains` | `value` : [text](#text-argument),
`stringComparison` : [enum](#enum-argument) | | `NotContains` | `Texts.NotContains` | `value` : [text](#text-argument),
`stringComparison` : [enum](#enum-argument) | | `StartsWith` | `Texts.StartsWith` | `value` : [text](#text-argument),
`stringComparison` : [enum](#enum-argument) | | `EndsWith` | `Texts.EndsWith` | `value` : [text](#text-argument),
`stringComparison` : [enum](#enum-argument) | | `Matches` | `Texts.Matches` | `pattern` : [text](#text-argument) | | `NotEmpty` | `Texts.NotEmpty` | - | | `NotWhiteSpace` | `Texts.NotWhiteSpace` | - | - Text length rules - When calculating length, `Environment.NewLine` is count as 1. | Fluent api | Message key | Args | | - | - | - | | `SingleLine` | `Texts.SingleLine` | - | | `ExactLength` | `Texts.ExactLength` | `length` : [number](#number-argument) | | `MaxLength` | `Texts.MaxLength` | `max` : [number](#number-argument) | | `MinLength` | `Texts.MinLength` | `min` : [number](#number-argument) | | `LengthBetween` | `Texts.LengthBetween` | `min` : [number](#number-argument),
`max` : [number](#number-argument) | - Email rules - `Email` rule has two modes, set by the enum value of type `Validot.EmailValidationMode` - `.Email(mode: EmailValidationMode.ComplexRegex)` is set by default (works the same as parameterless `.Email()`) and contains the regex-based logic copy-pasted from the [Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/standard/base-types/how-to-verify-that-strings-are-in-valid-email-format). - `.Email(mode: EmailValidationMode.DataAnnotationsCompatible)` checks only if the value contains a single `@` character in the middle, which is the logic used in the dotnet's [System.ComponentModel.DataAnnotations.EmailAddressAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute). - It's less accurate, but benchmarks show that it's about 6x faster while consuming 32% less memory. | Fluent api | Message key | Args | | - | - | - | | `Email` | `Texts.Email` | - | ### Times rules - Rules apply to `DateTime` and `DateTimeOffset`. - `TimeComparison` is the custom enum in Validot and describes the way time should be compared: - `All` - both date part and time part are compared. - `JustDate` - only date is compared (the time part is completely skipped) - `JustTime` - only time is compared (the date part is completely skipped) | Fluent api | Message key | Args | | - | - | - | | `Equalto` | `Times.Equalto` | `value` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `NotEqualto` | `Times.Equalto` | `value` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `After` | `Times.After` | `min` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `AfterOrEqualTo` | `Times.AfterOrEqualTo` | `min` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `Before` | `Times.Before` | `max` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `BeforeOrEqualTo` | `Times.BeforeOrEqualTo` | `max` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `Between` | `Times.Between` | `max` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | | `BetweenOrEqualTo` | `Times.BetweenOrEqualTo` | `min` : [time](#time-argument),
`max` : [time](#time-argument),
`timeComparison` : [enum](#enum-argument) | ### Guid rules - Rules apply to `Guid`. | Fluent api | Message key | Args | | - | - | - | | `EqualTo` | `GuidType.EqualTo` | `value` : [guid](#guid-argument) | | `NotEqualTo` | `GuidType.NotEqualTo` | `value` : [guid](#guid-argument) | | `NotEmpty` | `GuidType.NotEmpty` | `value` : [guid](#guid-argument) | ### TimeSpan rules - Rules apply to `TimeSpan`. - Most of them are same as for numbers, but with different message. | Fluent api | Message key | Args | | - | - | - | | `EqualTo` | `TimeSpanType.EqualTo` | `value` : [type](#type-argument) | | `NotEqualTo` | `TimeSpanType.EqualTo` | `value` : [type](#type-argument) | | `GreaterThan` | `TimeSpanType.Greater` | `min` : [type](#type-argument) | | `GreaterThanOrEqualTo` | `TimeSpanType.GreaterThanOrEqualTo` | `min` : [type](#type-argument) | | `LessThan` | `TimeSpanType.LessThan` | `max` : [type](#type-argument) | | `LessThanOrEqualTo` | `TimeSpanType.LessThanOrEqualTo` | `max` : [type](#type-argument) | | `Between` | `TimeSpanType.LessThan` | `min` : [type](#type-argument),
`max` : [type](#type-argument) | | `BetweenOrEqualTo` | `TimeSpanType.LessThanOrEqualTo` | `min` : [type](#type-argument),
`max` : [type](#type-argument) | | `NonZero` | `TimeSpanType.NonZero` | - | | `Positive` | `TimeSpanType.Positive` | - | | `NonPositive` | `TimeSpanType.NonPositive` | - | | `Negative` | `TimeSpanType.Negative` | - | | `NonNegative` | `TimeSpanType.NonNegative` | - | ### Custom rules - Custom rules should be based on [RuleTemplate](#ruletemplate) command, wrapped into an extension method. - The method needs to extend the `IRuleIn` interface, where `T` is the type of the object to be validated. - The method needs to return `IRuleOut`. - Both `IRuleOut` and `IRuleIn` ensure that the custom rule complies with the Validot's fluent api structures. - The namespace where the extension method is doesn't matter that much. - However, all built-in rules live in `Validot` namespace. ``` csharp public static class MyCustomValidotRules { public static IRuleOut HasCharacter(this IRuleIn @this) { return @this.RuleTemplate( m => m.Length > 0, "Must have at least one character!" ); } } ``` _Above, the definition of the custom rule `HasCharacters`. Below, the example os usage._ ``` csharp Specification specification = s => s .HasCharacter(); var validator = Validator.Factory.Create(specification); validator.Validate("test").AnyErrors; // false validator.Validate("").ToString(); // Must have at least one character! ``` - Custom rules can have arguments. - Please be extra careful with wrapping/boxing external references into the predicate. It might cause the memory leak, especially if the [validator](#validator) does exist as a singleton. - The pattern is: all method arguments should be available as [message arguments](#message-arguments) under the same names. ``` csharp public static IRuleOut HasCharacter( this IRuleIn @this, char character, int count = 1) { return @this.RuleTemplate( value => value.Count(c => c == character) == count, "Must have character '{character}' in the amount of {count}", Arg.Text(nameof(character), character), Arg.Number(nameof(count), count) ); } ``` ``` csharp Specification specification = s => s .HasCharacter('t', 2); var validator = Validator.Factory.Create(specification); validator.Validate("test").AnyErrors; // false validator.Validate("").ToString(); // Must have character 't' in the amount of 2 ``` - Instead of a message, you can provide a message key. Technically there is no difference, but it's easier for the user to overwrite the content. - The pattern for the message key is `Category.MethodName`. - Example; `EqualTo` for texts is `Texts.EqualTo` - Example; `GreaterThan` for numbers is `Numbers.GreaterThan` - Good to read: - [Rules](#rules) - list of built-in rules, along with their message keys and available arguments. - [Translations](#translations) - how translations work. ``` csharp public static IRuleOut HasCharacter( this IRuleIn @this, char character, int count = 1) { return @this.RuleTemplate( value => value.Count(c => c == character) == count, "Text.HasCharacter", Arg.Text(nameof(character), character), Arg.Number(nameof(count), count) ); } ``` ``` csharp Specification specification = s => s .HasCharacter('t', 2); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "Text.HasCharacter", "Must have character '{character}' in the amount of {count}") .WithTranslation("Polish", "Text.HasCharacter", "Musi zawierać znak '{character}' w ilości {count|culture=pl-PL}") ); validator.Validate("test").AnyErrors; // false var result = validator.Validate(""); result.ToString(); // Must have character 't' in the amount of 2 result.ToString(translationName: "Polish"); // Musi zawierać znak 't' w ilości 2 ``` ## Message arguments - [Error message](#message) might contain arguments in its content. - The placeholder for the argument value is using pattern: `{argumentName}`. - Arguments can be used in [WithMessage](#withmessage), [WithExtraMessage](#withextramessage) and [RuleTemplate](#ruletemplate). - The pattern followed in all [the built-in rules](#rules) is: the argument name is exactly the same as the method's argument. ``` csharp Specification specification = s => s .Between(min: 0.123M, max: 100.123M) .WithMessage("The number needs to fit between {min} and {max}"); var validator = Validator.Factory.Create(specification); validator.Validate(105).ToString(); // The number needs to fit between 0.123 and 100.123 ``` - Arguments can be parametrized: - Parameters follow format: `parameterName=parameterValue`. - Parameters are separated with `|` (vertical bar, pipe) character from the argument name and from each other. - Single parameter example: `{argumentName|parameterName=parameterValue}`. - Multiple parameters example: `{argumentName|param1=value1|param2=value2|param3=value3}`. ``` csharp Specification specification = s => s .Between(min: 0.123M, max: 100.123M) .WithMessage("The maximum value is {max|format=000.000}") .WithExtraMessage("The minimum value is {min|format=000.000|culture=pl-PL}"); var validator = Validator.Factory.Create(specification); validator.Validate(105).ToString(); // The maximum value is 100.123 // The minimum value is 000,123 ``` ### Enum argument - Types: all enums - Created with `Arg.Enum("name", value)`. - Parameters: - `format` - number format, the string that goes to [ToString](https://docs.microsoft.com/en-us/dotnet/api/system.enum.tostring?view=netstandard-2.0) method. - if not set, the default value is `G`. - `translation` - if set to `true`, placeholder is transformed into [translation argument](#translation-argument): `{_translation|key=messageKey}`. - the message key is in this format: `Enum.EnumFullTypeName.EnumValueName`. - ultimately, placeholder will be replace with text from the specification. - if `translation` is present, `format` is ignored. | Placeholder | Argument | Final form | | - | - | - | | `{arg}` | `StringComparison.Ordinal` | `Oridinal` | | `{arg\|format=G}` | `StringComparison.Ordinal` | `Oridinal` | | `{arg\|format=D}` | `StringComparison.Ordinal` | `4` | | `{arg\|format=X}` | `StringComparison.Ordinal` | `00000004` | | `{arg\|translation=true}` | `StringComparison.Ordinal` | `{_translation\|key=Enum.System.StringComparison.Ordinal}` | ``` csharp Specification gmailSpecification = s => s .EndsWith("@gmail.com", stringComparison: StringComparison.OrdinalIgnoreCase) .WithMessage("Must ends with @gmail.com {stringComparison|translation=true}"); var validator = Validator.Factory.Create(gmailSpecification, settings => settings .WithTranslation("English", "Enum.System.StringComparison.OrdinalIgnoreCase", "(ignoring case!)") ); validator.Validate("john.doe@outlook.com").ToString(); // Must ends with @gmail.com (ignoring case!) ``` _In the example above, [WithMessage](#withmessage) is using `{stringComparison|translation=true}` placeholder, which is - under the hood - transformed into [translation argument](#translation-argument) `{_translation|key=Enum.System.StringComparison.Ordinal}` and ultimately - replaced with the message registered under the key `Enum.System.StringComparison.Ordinal`._ - Good to read: - [translation argument](#translation-argument) - how to translation argument works. - [translations](#translations) - how translations work. ### Guid argument - Types: `Guid` - Created with `Arg.GuidValue("name", value)`. - Parameters: - `format` - guid format, the string that goes to [ToString](https://docs.microsoft.com/en-us/dotnet/api/system.guid.tostring?view=netstandard-2.0) method. - if not set, the default value is `D`. - `case` - available values: `upper`, `lower`. - calls `ToUpper` or `ToLower` method on the stringified guid value. | Placeholder | Argument | Final form | | - | - | - | | `{arg}` | `c2ce1f3b-17e5-412e-923b-6b4e268f31aa` | `c2ce1f3b-17e5-412e-923b-6b4e268f31aa` | | `{arg\|case=upper}` | `c2ce1f3b-17e5-412e-923b-6b4e268f31aa` | `C2CE1F3B-17E5-412E-923B-6B4E268F31AA` | | `{arg\|format=X}` | `c2ce1f3b-17e5-412e-923b-6b4e268f31aa` | `{0xc2ce1f3b,0x17e5,0x412e,{0x92,0x3b,0x6b,0x4e,0x26,0x8f,0x31,0xaa}}` | | `{arg\|format=X\|case=upper}` | `c2ce1f3b-17e5-412e-923b-6b4e268f31aa` | `{0XC2CE1F3B,0X17E5,0X412E,{0X92,0X3B,0X6B,0X4E,0X26,0X8F,0X31,0XAA}}` | ``` csharp Specification specification = s => s .NotEqualTo(new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")) .WithMessage("Must not be equal to: {value|format=X|case=upper}"); var validator = Validator.Factory.Create(specification); validator.Validate(new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")).ToString(); // Must not be equal to: {0XC2CE1F3B,0X17E5,0X412E,{0X92,0X3B,0X6B,0X4E,0X26,0X8F,0X31,0XAA}} ``` ### Number argument - Types: `int`, `uint`, `short`, `ushort`, `long`, `ulong`, `byte`, `sbyte`, `decimal`, `double`, `float` - Created with `Arg.Number("name", value)`. - Parameters: - `format` - guid format, the string that goes to the related [ToString](https://docs.microsoft.com/en-us/dotnet/api/system.int32.tostring?view=netstandard-2.0) method. - `culture` - culture code, the string that goes to the [CultureInfo.GetCultureInfo](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.getcultureinfo?view=netstandard-2.0#System_Globalization_CultureInfo_GetCultureInfo_System_String_) method. - If not set the default culture passed to ToString method is `CultureInfo.InvariantCulture` | Placeholder | Argument | Final form | | - | - | - | | `{arg}` | `123.987` | `123.987` | | `{arg\|format=X}` | `123` | `7B` | | `{arg\|format=0.00}` | `123.987` | `123.99` | | `{arg\|culture=pl-PL}` | `123.987` | `123,987` | | `{arg\|format=0.00\|culture=pl-PL}` | `123.987` | `123,99` | ``` csharp Specification specification = s => s .EqualTo(666.666M) .WithMessage("Needs to be equal to {value|format=0.0|culture=pl-PL}"); var validator = Validator.Factory.Create(specification); validator.Validate(10).ToString(); // Needs to be equal to 666,7 ``` ### Text argument - Types: `string`, `char` - Created with `Arg.Text("name", value)`. - Parameters: - `case` - available values: `upper`, `lower`. - calls `ToUpper` or `ToLower` method on the stringified guid value. - if not set, the value stays as it is | Placeholder | Argument | Final form | | - | - | - | | `{arg}` | `Bart` | `Bart` | | `{arg\|case=upper}` | `Bart` | `BART` | | `{arg\|case=lower}` | `Bart` | `bart` | ``` csharp Specification gmailSpecification = s => s .EndsWith("@gmail.com") .WithMessage("Must ends with: {value|case=upper}"); var validator = Validator.Factory.Create(gmailSpecification); validator.Validate("john.doe@outlook.com").ToString(); // Must ends with: @GMAIL.COM ``` ### Time argument - Types: `DateTime`, `DateTimeOffset`, `TimeSpan` - Parameters: - `format` - guid format, the string that goes to the related [ToString](https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tostring?view=netstandard-2.0) method. - The default time format: `HH:mm:ss.FFFFFFF` - The default date format: `yyyy-MM-dd` - The default date and time format: `HH:mm:ss.FFFFFFF yyyy-MM-dd` - `culture` - culture code, the string that goes to the [CultureInfo.GetCultureInfo](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.getcultureinfo?view=netstandard-2.0#System_Globalization_CultureInfo_GetCultureInfo_System_String_) method. - If not set the default culture passed to ToString method is `CultureInfo.InvariantCulture` | Placeholder | Argument | Final form | | - | - | - | | `{arg}` | `new DateTime(2000, 01, 15, 16, 04, 05, 06)` | `2000-01-15 16:04:05.006` | | `{arg\|case=upper}` | `new DateTime(2000, 01, 15, 16, 04, 05, 06)` | `2000-01-15T16:04:05` | | `{arg\|case=lower}` | `new DateTime(2000, 01, 15, 16, 04, 05, 06)` | `20000115` | ``` csharp Specification specification = s => s .Before(new DateTime(2000, 1, 2, 3, 4, 5, 6)) .WithMessage("Must not be before: {max|format=yyyy MM dd + HH:mm}"); var validator = Validator.Factory.Create(specification); validator.Validate(new DateTime(2001, 1, 1, 1, 1, 1, 1)).ToString(); // Must not be before: 2000 01 02 + 03:04 ``` ### Translation argument - Translation argument allows to include a phrase from the current translation. - It's always in this form: - `{_translation|key=MessageKey}` ``` csharp Specification specification = s => s .NotEqualTo(666) .WithMessage("!!! {_translation|key=TripleSix} !!!"); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "TripleSix", "six six six") .WithTranslation("Polish", "TripleSix", "sześć sześć sześć") ); var result = validator.Validate(666); result.ToString(translationName: "English"); // !!! six six six !!! result.ToString(translationName: "Polish"); // !!! sześć sześć sześć !!! ``` ### Type argument - Types: `Type` - Created with `Arg.Type("name", value)`. - Parameters: - `format` - available values: `name`, `fullName`, `toString`. - `name` - gets the type name, generics are nicely resolved. - `fullName` - gets the full type name, generics are nicely resolved. - `toString` - calls [ToString()](https://docs.microsoft.com/en-us/dotnet/api/system.type.tostring?view=netstandard-2.0). - if not sent, the default `format` value is `name`. - `translation` - if set to `true`, placeholder is transformed into [translation argument](#translation-argument): `{_translation|key=messageKey}`. - the message key is in this format: `Type.FullName`. - ultimately, placeholder will be replaced with text from the specification. - if `translation` is present, `format` is ignored. | Placeholder | Argument | Final form | | - | - | - | | `{arg}` | `typeof(int)` | `Int32` | | `{arg\|format=name}` | `typeof(int)` | `Int32` | | `{arg\|format=fullName}` | `typeof(int)` | `System.Int32` | | `{arg\|format=toString}` | `typeof(int)` | `System.Int32` | | `{arg}` | `typeof(int?)` | `Nulllable` | | `{arg\|format=name}` | `typeof(int?)` | `Nulllable` | | `{arg\|format=fullName}` | `typeof(int?)` | `System.Nulllable` | | `{arg\|format=toString}` | `typeof(int?)` | `System.Nullable'1[System.Int32]` | | `{arg\|translation=true}` | `typeof(int?)` | `{_translation\|key=Type.System.Nullable}` | ### Path argument - Path argument allows to include the [path](#path) of the validated value. - It's always in this form: - `{_path}` - It's more difficult to cache such messages (they are less deterministic), so overusing path arguments might slightly decrease the performance. - It doesn't contain parameters. ``` csharp Specification specification = s => s .Positive() .WithPath("Number.Value") .WithMessage("Number value under {_path} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString(); // Number.Value: Number value under Number.Value needs to be positive! ``` - In the case of the root path, the value is just an empty string. - And it might look weird in the final printing. ``` csharp Specification specification = s => s .Positive() .WithMessage("Number value under {_path} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString(); // Number value under needs to be positive! ``` ### Name argument - Name argument allows to include the name of the validated value. - Name is the last segment of the [path](#path). - Parameters: - `format` - available values: `titleCase`. | Placeholder | Path | Final form | | - | - | - | | `{_name}` | `someWeirdName123` | `someWeirdName123` | | `{_name\|format=titleCase}` | `someWeirdName123` | `Some Weird Name 123` | | `{_name}` | `nested.path.someWeirdName123` | `someWeirdName123` | | `{_name\|format=titleCase}` | `nested.path.someWeirdName123` | `Some Weird Name 123` | | `{_name}` | `path.This_is_a_Test_of_Network123_in_12_days` | `path.This_is_a_Test_of_Network123_in_12_days` | | `{_name\|format=titleCase}` | `path.This_is_a_Test_of_Network123_in_12_days` | `This Is A Test Of Network 123 In 12 Days` | - It's more difficult (and sometimes it's even impossible) to cache such messages (they are less deterministic), so overusing name arguments might slightly decrease the performance. ``` csharp Specification specification = s => s .Positive() .WithPath("Number.Primary.SuperValue") .WithMessage("The {_name} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString(); // Number.Primary.SuperValue: The SuperValue needs to be positive! ``` - Use `{_name|format=titleCase}` to get the name title cased. ``` csharp Specification specification = s => s .Positive() .WithPath("Number.Primary.SuperDuperValue123") .WithMessage("The {_name|format=titleCase} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString(); // Number.Primary.SuperDuperValue123: The Super Duper Value 123 needs to be positive! ``` - Similarly to [path argument](#path-argument), in case of the root path, the value is just empty string. ``` csharp Specification specification = s => s .Positive() .WithMessage("The {_name} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString(); // The needs to be positive! ``` ## Translations - From the purely technical perspective, messages used in the specification are not the error messages, but only the message keys. - It means that using [WithMessage](#withmessage), [WithExtraMessage](#withextramessage) and [RuleTemplate](#ruletemplate), you're setting the message key. - This also covers all of the default messages like the one if the required value is null. - The [validation result](#result) uses the translation process before returning the messages through its methods (e.g. [MessageMap](#messagemap) or [ToString](#tostring)). - The translation process step by step: - Get the translation dictionary using its name. - Look for the message key in the translation dictionary. - If the message key is present, return the value under the message key. - If the message key is not present, return the message key. ``` csharp Specification specification = s => s .Rule(m => m.Contains("@")).WithMessage("Must contain @ character"); var validator = Validator.Factory.Create(specification); validator.Validate("").ToString(); // Must contain @ character ``` _In the above code, [WithMessage](#withmessage) sets `"Must contain @ character"` message key for [Rule](#rule). However, there is no such message key in the standard, default `English` translation, so [ToString](#tostring) prints the original message key._ - Translation dictionary can be populated using [WithTranslation](#withtranslation) method of the [settings](#settings) object. ``` csharp Specification specification = s => s .Rule(m => m.Contains("@")).WithMessage("Must contain @ character"); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("Polish", "Must contain @ character", "Musi zawierać znak: @") .WithTranslation("English", "Must contain @ character", "Must contain character: @") ); var result = validator.Validate(model); result.ToString(); // Must contain character: @ result.ToString("Polish"); // Musi zawierać znak: @ ``` _In the above code, [WithMessage](#withmessage) sets `"Must contain @ character"` message key for [Rule](#rule). But this time, `"Must contain @ character"` key exists in both `Polish` and `English` dictionary (thanks to the [WithTranslation](#withtranslation) method). So the final validation [result](#result) contains phrases from the dictionaries, not from the [WithMessage](#withmessage)._ - Good to read: - [WithMessage](#withmessage), [WithExtraMessage](#withextramessage) and [RuleTemplate](#ruletemplate) - commands that set message keys. - [WithTranslation](#withtranslation) - setting entries in the translation dictionary. - [Message arguments](#message-arguments) - about message arguments (yes, translation phrases can use them!). ### Built-in translations - Validot includes some translations out of the box. Technically they are nothing more than extensions that under the hood add phrases to the Validator's settings using [WithTranslation](#withtranslation) method. - You're more than welcome if you want to contribute new built-in translations to Validot. The process is briefly described in [CONTRIBUTING document](CONTRIBUTING.md#Translations). - If you want just to create custom translation for your project only, see [Custom translation](#custom-translation) section. #### WithPolishTranslation - The Spanish translation name is just `"Polish"` - It can be included using `WithPolishTranslation()` extension. ``` csharp Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() ); validator.Validate(null).ToString(translationName: "Polish"); // Wymagane validator.Validate("").ToString(translationName: "Polish"); // Musi nie być puste validator.Validate("1234567890").ToString(translationName: "Polish"); // Musi być długości maksymalnie 5 znaków ``` #### WithSpanishTranslation - The Spanish translation name is just `"Spanish"` - It can be included using `WithSpanishTranslation()` extension. ``` csharp Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithSpanishTranslation() ); validator.Validate(null).ToString(translationName: "Spanish"); // Requerido validator.Validate("").ToString(translationName: "Spanish"); // No debe estar vacío validator.Validate("1234567890").ToString(translationName: "Spanish"); // Debe tener como máximo 5 caracteres ``` #### WithRussianTranslation - The Russian translation name is just `"Russian"` - It can be included using `WithRussianTranslation()` extension. ``` csharp Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithRussianTranslation() ); validator.Validate(null).ToString(translationName: "Russian"); // Требуется validator.Validate("").ToString(translationName: "Russian"); // Не должен быть пуст validator.Validate("1234567890").ToString(translationName: "Russian"); // Должен быть не больше 5 символов в длину ``` #### WithPortugueseTranslation - The Portuguese translation name is just `"Portuguese"` - It can be included using `WithPortugueseTranslation()` extension. ``` csharp Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithPortugueseTranslation() ); validator.Validate(null).ToString(translationName: "Portuguese"); // Obrigatório validator.Validate("").ToString(translationName: "Portuguese"); // Não deve estar vazio validator.Validate("1234567890").ToString(translationName: "Portuguese"); // Deve ter no máximo 5 caracteres ``` #### WithGermanTranslation - The Portuguese translation name is just `"German"` - It can be included using `WithGermanTranslation()` extension. ``` csharp Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithGermanTranslation() ); validator.Validate(null).ToString(translationName: "German"); // Obrigatório validator.Validate("").ToString(translationName: "German"); // Não deve estar vazio validator.Validate("1234567890").ToString(translationName: "German"); // Deve ter no máximo 5 caracteres ``` ### Overriding messages - Overriding the default error messages follows the process described in the main [Translations](#translations) section. - The only missing bit of information is; what are the message key of the default messages? - And the answer is; there are all listed in [Rules](#rules) section (column `Message key`). - If you want to override some default error message, find it in the [Rules](#rules) section and provide the new value for it using [WithTranslation](#withtranslation). ``` csharp Specification specification = s => s .NotEmpty(); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "Global.Required", "String cannot be null!") .WithTranslation("English", "Texts.NotEmpty", "String cannot be empty!") ); validator.Validate(null).ToString(); // String cannot be null! validator.Validate("").ToString(); // String cannot be empty! ``` _Above code presents how to override the default error messages of `NotEmpty` - according to the [Rules](#rules) section, it uses `Texts.NotEmpty` message key._ - Translation phrases can use [message arguments](#message-arguments). - Similarly to message keys, arguments along with their types are listed in the [Rules](#rules) section of this doc. ``` csharp Specification specification = s => s .BetweenOrEqualTo(16.66M, 666.666M); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation( "English", "Numbers.BetweenOrEqualTo", "Only numbers between {min|format=000.0000} and {max|format=000.0000} are valid!") ); validator.Validate(10).ToString(); // Only numbers between 016.6600 and 666.6660 are valid! ``` _`BetweenOrEqualTo` uses message key `Numbers.BetweenOrEqualTo` and two [number arguments](#number-argument): `min` and `max`._ ### Custom translation - Custom translation is nothing more than a translation dictionary that delivers phrases for all the default message keys. - `English` translation is the default one, always present in the validator, and it contains all of the phrases. - To create your own custom translation within your project, you can copy-paste and adjust the following code; - [EnglishTranslation.cs](../src/Validot/Translations/English/EnglishTranslation.cs) - dictionary with all the message keys. - [EnglishTranslationsExtensions.cs](../src/Validot/Translations/English/EnglishTranslationsExtensions.cs) - extension method. - The pattern is to create extension method to the [settings object](#settings) that wraps [WithTranslation](#withtranslation) calls, delivering phrases for all of the [rules](#rules). ``` csharp public static class WithYodaEnglishExtension { public static ValidatorSettings WithYodaEnglish(this ValidatorSettings @this) { var dictionary = new Dictionary() { ["Global.Required"] = "Exist, it must.", // more phrases ... ["Numbers.LessThan"] = "Greater than {max}, the number must, be not." // more phrases ... }; return @this.WithTranslation("YodaEnglish", dictionary); } } ``` _Above, the extension that applies the translation dictionary using [WithTranslation](#withtranslation). Below, the example of usage:_ ``` csharp Specification specification = s => s .LessThan(10); var validator = Validator.Factory.Create(specification, settings => settings .WithYodaEnglish() ); validator.Validate(null).ToString("YodaEnglish"); // Exist, it must. validator.Validate(20).ToString("YodaEnglish"); // Greater than 10, the number must, be not. ``` - Good to read: - [Built-in translations](#built-in-translations) - translations Validot delivers out of the box. - [Contributing with new translations](CONTRIBUTING.md#translations) - how to contribute with a new translation to the Validot project. - [Translations](#translations) - how translations works. - [Overriding messages](#overriding-messages) - how to override messages. - [Rules](#rules) - list of message keys and arguments. - [WithTranslation](#withtranslation) - how to set translation phrase. ## Development - The build system is based on the [nuke.build](https://nuke.build/) project. - This section contains examples that uses powershell, but bash scripts are also fully supported. - Just replace `pwsh build.ps1` with `bash build.sh` - If you're keep experiencing compilation errors that your IDE doesn't show (and at the same time `dotnet build` completes OK), consider adding `--AllowWarnings`. - By default, the build system requires the code to follow the rules set in [editorconfig](../.editorconfig). - If you don't provide `--Version` parameter (value needs to follow [semver](https://semver.org/) rules), the default version is `0.0.0-XHHmmss`, where `X` is the day of the current year, `HHmmss` is the timestamp. ### Build - Compile the project with the tests: - `pwsh build.ps1` - This is the same as `pwsh build.ps1 --target Compile` - Create nuget package: - `pwsh build.ps1 --target NugetPackage --Version A.B.C --Configuration Release` - Replace `A.B.C` with the semver-compatible version number. - The nuget package version will be `A.B.C`. - `AssemblyVersion` will be `A.0.0.0`. - `AssemblyFileVersion` will be `A.B.C.0`. - The package appears in `artifacts/nuget` directory. - Clean the project: - `pwsh build.ps1 --target Clean` - Deletes all of the `bin` and `obj` directories in the solution. - Reset everything. - `pwsh build.ps1 --target Reset` - Restores the original `TargetFramework` in the test projects. - Deletes all diretories created by the build project (`tools`, `artifacts`, etc.). - Also, triggers `Clean` target at the end. ### Tests - Run tests: - `pwsh build.ps1 --target Tests` - The detailed result files (`junit` format) appear in `artifacts/tests` directory. - Run tests on specific framework: - `pwsh build.ps1 --target Tests --DotNet netcoreapp2.1` - `pwsh build.ps1 --target Tests --DotNet net48` - It sets the `TargetFramework` in the test projects' csproj files. - You can use the framework id (`netcoreapp3.1`), as well as the sdk version (`3.1.100`) - the highest framework id version available in the sdk will be used. - Get code coverage report: - `pwsh build.ps1 --target CodeCoverageReport` - HTML and JSON reports will appear in `artifacts/coverage_reports` directory. - During this task, the dotnet global tool `dotnet-reportgenerator-globaltool` is installed locally in `tools` directory. - Reports are tracking history! - The history data is in `artifacts/coverage_reports/_history` directory. - Get code coverage data: - `pwsh build.ps1 --target CodeCoverage` - The opencover file will appear in `artifacts/coverage` directory. ### Benchmarks - Run all benchmarks: - `pwsh build.ps1 --target Benchmarks` - It would take several minutes to complete the execution. - The results will appear in `artifacts/benchmarks` directory. - By default, the benchmarks are run as `short` jobs. - Run all benchmarks, better: - `pwsh build.ps1 --target Benchmarks --FullBenchmark` - This mode doesn't set job to `short`. - It depends on your machine, but you can assume that it would finish in about 1-2 hours. - Run benchmarks selectively: - `pwsh build.ps1 --target Benchmarks --BenchmarksFilter "X"` - `X` is the full name of the benchmark method: `namespace.typeName.methodName`. - Wildcards are accepted, so `pwsh build.ps1 --target Benchmarks --BenchmarksFilter "*NoErrors*"` would execute all methods inside [`NoErrorsBenchmark.cs`](../tests/Validot.Benchmarks/Comparisons/NoErrorsBenchmark.cs). - Can be combined with `--FullBenchmark`. - Benchmarks are based on [benchmarkdotnet](https://benchmarkdotnet.org). ================================================ FILE: docs/articles/crafting-model-specifications-using-validot.md ================================================ --- title: Crafting model specifications using Validot date: 2022-09-26 06:00:00 --- The Validot project comes with comprehensive documentation, excellent for looking up how certain features work. On the other hand, documentations don't always serve as great tutorials or walkthrough guides, which in Validot is very much the case. This blog post aspires to answer this problem. Following the real-life use case, we'll proceed step-by-step with creating a fully-featured specification using Validot's advanced fluent-driven interface. ## Specification Imagine having a web form that allows users to sign up. There is a text field for the one's name that's supposed to be validated. We'll start with the bare minimum form of Validot's specification: ``` csharp Specification nameSpecification = s => s; ``` `Specification` describes a valid state of `T` instances. Struct or class, custom type, or something from the framework's built-in namespace - `T` has no constraints and could be virtually anything you need it to be. Above, `Specification` describes a valid string object and the assigned value (`s => s`) is a fluent API expression builder, a method chain that, in this particular case, stands empty. Methods that are part of the chain are called _commands_. By default, Validot requires the value to be non-null, so even having no explicitly defined commands, the specification is correct and says the following: "there are no rules for a string value, but the value itself needs to be non-null". An empty specification can hardly play a role in a real-life scenario, so how about adding some logic: ``` csharp Specification nameSpecification = s => s .Rule(name => name.Length > 3); ``` `Rule` is an essential fluent API command because it allows defining a custom validation code. It takes the regular dotnet's `Predicate` that receives the analyzed value and is supposed to return `true` if it's valid and `false` if otherwise. Having said that, the above snippet could easily be replaced by: ``` csharp Predicate mustHaveMoreThanThreeCharacters = text => text.Length > 3; Specification nameSpecification = s => s .Rule(mustHaveMoreThanThreeCharacters); ``` There is no sanctioned way of doing this. Sometimes the code might benefit from defining predicates separately (e.g., when you want to utilize one in multiple places), but in general - it's totally up to your personal preference. ## Validator So, at this point, we have a specification that says "a string value must be present (non-null) and must have more than three characters". However, specification on its own is merely a definition of the valid state. Not much less than a bunch of instructions on distinguishing a valid entity from an invalid. Executing these instructions against objects is the validator's job. You can create one by calling the static factory `Validator.Factory.Create`, where the generic argument `T` comes from the delivered `Specification`, and then lands in the produced `Validator`: ``` csharp var validator = Validator.Factory.Create(nameSpecification); validator.IsValid("Alice"); // true validator.IsValid("Bob"); // false var result = validator.Validate("Bob"); result.ToString(); // Invalid ``` That's right: `Validator` can process only objects of type `T`, according to the single `Specitication` it was created with (nothing stops you from creating multiple validators, though). In exchange, it offers immutability, thread-safety and exceptionally high performance of work, which it does using its two methods `IsValid` and `Validate`. `IsValid` is the ultra-optimized way to make quick correctness checks. It's super-fast, allocates virtually nothing on the heap, but it has its price: `IsValid` delivers no information other than a simple boolean flag. So, if you don't care about the details and need to drive your app's logic according to only a binary validation result, using `IsValid` makes the most sense. `Validate`, on the other hand, delivers a more comprehensive report. For now, we're interested in its overloaded `ToString()` method that prints all error messages (prefixed with their path) in separate lines. In the above snippet, we see only a single `"Invalid"`, the default error message for the `Rule` command. You can alter its content by placing another command - `WithMessage` - directly after the related `Rule`. Like this: ``` csharp Specification nameSpecification = s => s .Rule(name => name.Length > 3).WithMessage("Min length is 4 characters"); ``` ``` csharp var validator = Validator.Factory.Create(nameSpecification); validator.IsValid("Alice"); // true validator.IsValid("Bob"); // false validator.Validate("Bob").ToString(); // Min length is 4 characters ``` ## Command types In theory, a specification can consist of three types of command: * scope commands - to wrap the validation logic (e.g., `Rule`, where we passed our predicate) * parameter commands - to parametrize other commands (e.g., `WithMessage`, which we used to overwrite the `Rule`'s error message) * presence commands - to set if the value is required or not (by default, it's always required, but of course, you can alter this behavior). In practice, it's much simpler than it might look at first glance. In Validot, there are only two so-called presence commands (`Optional` and `Required`), and they can be placed only at the beginning of the chain. Therefore, if the specification doesn't start with `Optional()`, then it by default behaves like it would with `Required()`. Secondly, all parameter commands start with `With…`, and they affect the closest preceding command that holds validation logic. ``` .RuleA().WithX(...).WithY(...) .RuleB().WithZ(...) ``` In the above hypothetical example, `RuleA` is affected by `WithX` and `WithY`, and respectively - `RuleB` is affected by `WithZ`. There are quite a few parameter commands and the [documentation describes them](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md){:target="_blank"} very intensively. `WithMessage`, for instance, overwrites the entire output with a single message. Ultimately, that's what happened - `Rule'`s default error content got replaced with `"Min length is 4 characters"`. ## Chaining commands We already know that specification can contain multiple scope commands, each with its set of aligned parameter commands. This is how it could be arranged in our example: ``` csharp Specification nameSpecification = s => s .Rule(name => name.Length > 3).WithMessage("Min length is 4 characters") .Rule(name => name.Length < 16).WithMessage("Max length is 15 characters") .Rule(name => name.Any() && char.IsUpperCase(name.First())).WithMessage("Must start with a capital letter"); ``` When you look at it, it's a reasonably well-structured method chain: each line wrapping a single rule and all of its modifiers. But most code editors will try to break it according to the one-method-one-line style. To maintain readability, you might consider using `And()` to visually separate the subsequent scope commands. `And` command contains no logic and does nothing - its sole purpose is to break method chains into groups: ``` csharp Specification nameSpecification = s => s .Rule(name => name.Length > 3) .WithMessage("Min length is 4 characters") .And() .Rule(name => name.Length < 16) .WithMessage("Max length is 15 characters") .And() .Rule(name => name.Any() && char.IsUpperCase(name.First())) .WithMessage("Must start with a capital letter"); ``` Both the above specifications are logically the same: ``` csharp var validator = Validator.Factory.Create(nameSpecification); validator.IsValid("Alice"); // true validator.IsValid("alice"); // false validator.Validate("bob").ToString(); // Min length is 4 characters // Must start with a capital letter validator.Validate("Elizabeth Alexandra Mary").ToString(); // Max length is 15 characters ``` Notice that the string value gets tested against all of the `Rule` commands, no matter how many declared failures. This is because an error doesn't stop the validation. Instead, all error outputs are collected and appear in the final report. ## Predefined rules Validot is shipped with dozens of predefined rules, ready to be included within specifications. You can safely assume that they are single-commands combining `Rule` with a dedicated predicate, followed by `WithMessage` with an appropriate error description. Naturally, you'd still need to write custom predicates for more complex scenarios; however the most common cases are covered pretty well. The complete list is available [in the documentation](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md){:target="_blank"} and constantly gets extended with new releases. We'll replace the first four commands in our example with the predefined `MinLength` and `MaxLength`: ``` csharp Specification nameSpecification = s => s .MinLength(4) .MaxLength(15) .And() .Rule(name => name.Any() && char.IsUpperCase(name.First())) .WithMessage("Must start with a capital letter"); var validator = Validator.Factory.Create(nameSpecification); validator.Validate("bob").ToString(); // Must be at least 15 characters in length // Must start with a capital letter validator.Validate("Elizabeth Alexandra Mary").ToString(); // Must be at most 15 characters in length ``` If for whatever reason, you don't like the error message that comes with a predefined rule, you can always overwrite it by placing `WithMessage` directly after it in the fluent API method chain: ``` csharp Specification nameSpecification = s => s .MinLength(min: 4) .WithMessage("Min length is {min} characters") .And() .MaxLength(max: 15) .WithMessage("Max length is {max} characters") .And() .Rule(name => name.Any() && char.IsUpperCase(name.First())) .WithMessage("Must start with a capital letter"); var validator = Validator.Factory.Create(nameSpecification); validator.Validate("bob").ToString(); // Min length is 4 characters // Must start with a capital letter validator.Validate("Elizabeth Alexandra Mary").ToString(); // Max length is 15 characters ``` In Validot, you can peek at the rule's argument name and use it inside the message in a placeholder wrapped with curly brackets. Or more figuratively speaking: the `min` parameter from `.MinLength(min: 4)` goes to `{min}` in the message, so effectively `.WithMessage("Min length is {min} characters")` prints out `"Min length is 4 characters"`. What's even more interesting, placeholders support parameters, so if `min` is of numeric type , `.WithMessage("Min length is {min|format=000.000|culture=pl-PL} characters")` would output `"Min length is 004,000 characters"` in the above code snippet. Message arguments are extensively developed, and the [documentation explains all aspects of the formatting](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#message-arguments){:target="_blank"}, plus contains [the complete list of built-in rules along with their parameters](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#rules){:target="_blank"}. ## Validating members So far, we've been taking care of a simple, single string. In practice, though, you'll be facing multi-level objects with all types of properties: collections, nullables and enums, and - of course - nested models, which means combination of all of these options. Validot allows to easily nest specifications and gracefully handle scenarios containing such a real world's variety. At first, we'll try to model the case in the first paragraph (about the user trying to sign up): ``` csharp class Contact { public string EmailAddress { get; set; } public bool SubscribedToNewsfeed { get; set; } public bool? TermsAndConditionsAccepted { get; set; } } class User { public string FirstName { get; set; } public IEnumerable MiddleNames { get; set; } public string LastName { get; set; } public int? Age { get; set; } public Contact Contact { get; set; } } ``` To validate a model's property, use the `Member` command with two arguments: the first is the expression selecting this property, and the second is the specification for it. Since we already have `nameSpecification` done, we can apply it to both `FirstName` and `LastName` members: ``` csharp Specification userSpecification = user => user .Member(m => m.FirstName, nameSpecification) .Member(m => m.LastName, nameSpecification); ``` `Member` opens a new nested scope (and that's why it's called a scope command) that works independently from its parent. All spotted errors will be saved with an additional information about the exact path where it occurred. For example, the result's `ToString()` printing will include it directly before each message. Like this: ``` csharp var user = new User { FirstName = "Elizabeth Alexandra Mary", LastName = "bob" }; var result = validator.Validate(user); result.ToString(); // FirstName: Min length is 4 characters // FirstName: Must start with a capital letter // LastName: Max length is 15 characters ``` The result contains also `MessageMap` dictionary that for each path holds the collection of errors, so you can examine them independently, one by one: ``` csharp var lastNameErrors = result.MessageMap["FirstName"]; // lastNameErrors.Length == 2 // lastNameErrors[0] == "Min length is 4 characters" // lastNameErrors[1] == "Must start with a capital letter" ``` You can also pass an inline specification to the `Member` command, as well as introduce more and more nested levels according to your needs and the model's structure. ``` csharp Specification userSpecification = user => user .Member(u => u.FirstName, nameSpecification) .Member(u => u.LastName, nameSpecification) .And() .Member(u => u.Contact, u => u .Member(c => c.EmailAddress, c => c .Email() .WithMessage("Email is very invalid") .And() .MaxLength(30) ) ); ``` ``` csharp var user = new User { FirstName = "Elizabeth", LastName = "Smith", Contact = new Contact() { EmailAddress = "invalid_email" } }; var result = validator.Validate(user); result.ToString(); // Contact.EmailAddress: Email is very invalid ``` Paths use dot as a separator between the property names: ``` csharp result.MessageMap["Contact.EmailAddress"].Length // 1 result.MessageMap["Contact.EmailAddress"].Single() == "Email is very invalid" // true ``` Because `MessageMap` is a regular dotnet's dictionary, you can verify a specific property easily with the built-in `ContainsKey` method: ``` csharp result.MessageMap.ContainsKey("Contact.EmailAddress") // true ``` ## Validating collections Although `Member` can handle all types of nested structures, collections are a bit tricky. For instance, `Member` requires a fixed path to a specific object, but you can't possibly know how many items the collection has when constructing specification. Of course, you could receive the entire collection in the `Rule`'s predicate, but how to apply another specification for each item? And how would the path of their eventual error messages look like? Validot comes to the rescue with `AsCollection` command, available for all objects that derive from `IEnumerable`. ``` csharp Specification> middleNamesCollectionSpec = m => m .AsCollection(nameSpecification); ``` Similarly to `Member`, `AsCollection` is a scope command, which means that an independent context (scope) is opened for the nested objects. Only this time, there are multiple scopes, because `AsCollection` applies the delivered specification to the all items acquired - one by one - from the enumerator. The path for their error output is `#n`, where `n` is the index under which they have been yielded to validation. ``` csharp var validator = Validator.Factory.Create(middleNamesCollectionSpec); var middleNames = new[] { "bob", "Elizabeth Alexandra Mary" }; validator.Validate(middleNames).ToString(); // #0: Min length is 4 characters // #0: Must start with a capital letter // #1: Max length is 15 characters ``` Naturally, next to `AsCollection` you can place other custom and [built-in rules](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#collections-rules) to validate the collection as a whole. For example, `MaxCollectionSize` to specify the maximum number of objects it could have. ``` csharp Specification userMiddleNamesSpecification = user => user .Member(u => u.MiddleNames, names => names .AsCollection(nameSpecification) .And() .MaxCollectionSize(3) .WithMessage("Maximum three middle names are allowed") ); var user = new User { MiddleNames = new [] { "bob", "Alexander", "patrick", "Al" }; } var validator = Validator.Factory.Create(userMiddleNamesSpecification); var result = validator.Validate(user); result.ToString(); // MiddleNames.#0: Min length is 4 characters // MiddleNames.#0: Must start with a capital letter // MiddleNames.#2: Must start with a capital letter // MiddleNames.#3: Min length is 4 characters // MiddleNames: Maximum three middle names are allowed ``` ## Validating nullables Nullables may not be as tricky as collections; still, it would be handy to have the underlying value unwrapped and validated effortlessly. And that's the purpose of yet another scope command - `AsNullable`. In our example, `User` contains the `Age` property of type `Nullable` (`int?`). Using `AsNullable` we can write the specification for just `int` and apply it directly to `Age`. ``` csharp Specification ageSpecification = age => age .GreaterThanOrEqualTo(0).WithMessage("Age must be 0 or more") .LessThan(129).WithMessage("People don't live that long"); Specification userAgeSpecification = user => user .Member(u => u.Age, age => age.AsNullable(ageSpecification)); ``` Technically speaking, `AsNullable` unwraps the `T` value out from `Nullable` and validates it according to the given `Specification`. In contrast to `AsCollection` and `Member`, `AsNullable` saves the error output under the same path. Therefore, you shouldn't expect `Age.Value` path in the results, but just `Age`. ``` csharp var result = validator.Validate(new User { Age = 140; }); result.ToString(); // Age: People don't live that long ``` The above code presents usage of `AsNullable`, however in this particular case you can achieve the same results without it. In our example, `ageSpecification` uses only built-in rules, without any extra custom logic. And all built-in rules for the value types Validot delivers in both standard and nullable version. Therefore, `GreaterThanOrEqualTo` and `LessThan` could be applied directly, like this: ``` csharp Specification userAgeSpecification = user => user .Member(u => u.Age, age => age .GreaterThanOrEqualTo(0).WithMessage("Age must be 0 or more") .LessThan(129).WithMessage("People don't live that long") ); ``` Of course, in other cases (e.g., custom logic and reusing already prepared specifications), `AsNullable` makes your life easier. ## Making a value optional The last command type is called a presence command and includes commands like `Required()` and `Optional()`. Their names are pretty self-explanatory, but just to have it on paper: their sole purpose is to define whether null is an acceptable case or an error. As previously mentioned, in Validot everything is required to be non-null by default, so it doesn't matter whether you start with `Required()` or not. These two specifications below are equal to each other: ``` csharp Specification userAgeSpecification1 = user => user .Member(u => u.Age, age => age .AsNullable(ageSpecification) ); Specification userAgeSpecification2 = user => user .Member(u => u.Age, age => age .Required() .AsNullable(ageSpecification) ); var validator1 = Validator.Factory.Create(userAgeSpecification1); var validator2 = Validator.Factory.Create(userAgeSpecification1); validator1.Validate(new User { Age = null }).ToString(); // Age: Required validator2.Validate(new User { Age = null }).ToString(); // Age: Required ``` However, explicitly using the `Required` command in the specification lets you define a custom error output in case of null. ``` csharp Specification userAgeSpecification = user => user .Member(u => u.Age, age => age .Required() .WithMessage("Information about age is mandatory for all user profiles") .And() .AsNullable(ageSpecification) ); var validator = Validator.Factory.Create(userAgeSpecification); validator.Validate(new User { Age = null }).ToString(); // Age: Information about age is mandatory for all user profiles ``` The opposition to `Required` is - of course - `Optional` and it makes a no-value acceptable. In case of null nothing happens (no error is recorded) and validation proceeds with further commands. ``` csharp Specification optionalUserAgeSpecification = user => user .Member(u => u.Age, a => a .Optional() .AsNullable(ageSpecification) ); var validator = Validator.Factory.Create(optionalUserAgeSpecification); validator.Validate(new User { Age = null }).AnyErrors; // false validator.Validate(new User { Age = 18 }).AnyErrors; // false validator.Validate(new User { Age = 188 }).ToString(); // Age: People don't live that long ``` ## Merging and extending specifications So far in this post, we've created quite a few specifications for the `User` class while describing different types of Validot commands. The first one, `userSpecification`, shows how to validate members, but there have also been `userMiddleNamesSpecification` for collections and `optionalUserAgeSpecification` for the optional nullable values. Is there a way to glue them together so we don't need to duplicate the code? There is. In theory, `AsModel` is the command that applies the delivered specification directly to the current value. Effectively, it allows the user to merge multiple specifications into one: ``` csharp Specification finalUserSpecification = s => s .AsModel(userSpecification) .AsModel(userMiddleNamesSpecification) .AsModel(optionalUserAgeSpecification); ``` The presented above `finalUserSpecification` works the same as it had all commands from `userSpecification`, `userMiddleNamesSpecification`, and `optionalUserAgeSpecification` copy-pasted one after another. Let's present it: ``` csharp var user = new User { MiddleNames = new [] { "bob", "Alexander", "patrick", "Al" }; LastName = "smith", Contact = new Contact() { EmailAddress = "invalid_email" }, Age = 200, }; var validator = Validator.Factory.Create(finalUserSpecification); validator.Validate(user).ToString(); // FirstName: Required // MiddleNames.#0: Min length is 4 characters // MiddleNames.#0: Must start with a capital letter // MiddleNames.#2: Must start with a capital letter // MiddleNames.#3: Min length is 4 characters // MiddleNames: Maximum three middle names are allowed // LastName: Must start with a capital letter // Contact.EmailAddress: Email is very invalid // Age: People don't live that long ``` Naturally, `AsModel` is a regular Validot command, so you can follow it with another, extending the specification with more and more logic. ``` csharp Specification finalUserSpecification = s => s .AsModel(userSpecification) .AsModel(userMiddleNamesSpecification) .AsModel(optionalUserAgeSpecification) .Rule(m => m.FirstName != m.LastName).WithMessage("First and last name must be different!"); ``` ``` csharp var user = new User { FirstName = "Michael", LastName = "Michael", }; var validator = Validator.Factory.Create(finalUserSpecification); validator.Validate(user).ToString(); // Contact: Required // MiddleNames: Required // First and last name must be different! ``` ## Overwriting error output Imagine a specification that produces very extensive and comprehensive report with lot of potential errors. We want to reuse it to validate collection of objects, but we don't care that much about the details, as with large set of items they are nothing but a noise. Validot allows error output overwriting and in fact, we've been using it across all of this post's code snippets. It's `WithMessage`! Let's look back at the first example it appeared: ``` csharp Specification nameSpecification = s => s .Rule(name => name.Length > 3).WithMessage("Min length is 4 characters"); ``` `Rule` has its default behavior and returns `"Invalid"` message, but we overwrote it with a custom one. `WithMessage`, as all parameter commands, affect the closest preceding scope command. In the above example, it's `Rule`, but it could be anything. What happens if `AsCollection` is followed by `WithMessage` in the specification handling the user middle names? ``` csharp Specification userMiddleNamesSpecification = user => user .Member(u => u.MiddleNames, names => names .AsCollection(nameSpecification) .WithMessage("Contains invalid name") .And() .MaxCollectionSize(3) .WithMessage("Maximum three middle names are allowed") ); ``` ``` csharp var user = new User { MiddleNames = new [] { "bob", "Alexander", "patrick", "Al" }; }; var validator = Validator.Factory.Create(finalUserSpecification); validator.Validate(user).ToString(); // MiddleNames: Contains invalid name // MiddleNames: Maximum three middle names are allowed ``` No matter how many invalid values are detected in `MiddleNames` collection, the output from `AsCollection` contains only a single message. This trick works on all scope commands, including `AsModel`: ``` csharp Specification finalUserSpecification = s => s .AsModel(userSpecification) .AsModel(userMiddleNamesSpecification).WithMessage("Invalid collection of middle names") .AsModel(optionalUserAgeSpecification) .Rule(m => m.FirstName != m.LastName).WithMessage("First and last name must be different!"); ``` ``` csharp var user = new User { FirstName = "John", MiddleNames = new [] { "bob", "Alexander", "patrick", "Al" }; LastName = "Smith", Contact = new Contact() { EmailAddress = "valid@address" }, }; var validator = Validator.Factory.Create(finalUserSpecification); validator.Validate(user).ToString(); // Invalid collection of middle names ``` ## Validot is so much more The list of possibilities doesn't end here. After reading this post and getting familiar with the philosophy of crafting specifications, you can go on and explore the full set of features detaily described on the [official documentation](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md){:target="_blank"}. Just to give a quick overview of what more to expect; you can - [append messages](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#withextramessage){:target="_blank"} to the existing error output instead of overwriting everything - return [error codes](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#code){:target="_blank"} instead of just messages - prepare [translations](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#translations){:target="_blank"} for the error messages (also there are [few translations](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#built-in-translations){:target="_blank"} available out of the box) - [overwrite paths](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#withpath){:target="_blank"} of the error output - [forbid](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#forbidden){:target="_blank"} the value to be present (set up null as the only valid case) - [convert the value](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#asconverted){:target="_blank"} to something different (even of different type) before validating it further - prepare your own [custom, reusable rules](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#custom-rules){:target="_blank"} with the same mechanism that the Validot's internal are using And much, much more. Validot is an open-source, MIT-licenced, fully tested, and documented project, hosted entirely on [github](https://github.com/bartoszlenar/Validot){:target="_blank"}. Type ``` dotnet add package Validot ``` and give it a try in your next dotnet-based microservice. ================================================ FILE: docs/articles/validots-performance-explained.md ================================================ --- title: Validot's performance explained date: 2020-10-07 06:00:00 --- ## A three-layer cake The road to achieving the performance goals kicked off elegantly - with a whiteboard. To visually grasp the problem, I wrote down all possible actions from the moment user defines the object's acceptable state until they can interpret the results. Consequently, it's easier to understand the validation flow, recognize usage patterns, categorize the activities, and notice relationships between them. The first noteworthy distinction among the listed steps divides those that will be executed during each validation, and those triggered only once. Some kinds of information will inevitably depend on the validated model (_is the value correct?_), while the other can be precalculated and reused in the subsequent calls (_what is the error message?_). Furthermore, some actions are optional because why would the library care about preparing a human-readable report if the user only wants to review the `IsValid` bool flag ? Using [FluentValidation](https://fluentvalidation.net/){:target="_blank"} (a great lib, by the way), how often did you see the code similar to this one below? ``` csharp // trigger fully-featured validation process var validationResult = validator.Validate(input); // and use just a fraction of the acquired information... if (!validationResult.IsValid) { // ...only to immediately terminate the execution (in one way or another) throw Exception(); } ``` Having the best performance level as the primary objective, Validot's API needed to be designed to avoid such situations easily. Under the hood, it came down to introducing lazy loading wherever possible, splitting the process into reasonably separate stages, and caching information more aggressively, even at the cost of fewer features. Soon enough, Validot became an excellent looking, three-layer cake. ## Building scheme The first tier starts right in the validator's constructor and is all about the specification analysis. Validot has the fluent interface exposed to the end-user, but each validator internally stores the rules in its own format, called the scheme. Consider it as a compiled specification; created because it's faster to check a bool flag `IsOptional` than to verify whether the `Optional()` method has been executed within the chain. It's the same set of rules and logic, but in the form of a cachable object that can be accessed and processed more efficiently. An even more critical operation is the message construction. The scheme contains all error contents with the inline arguments resolved, for all given translations. What does it imply? ``` csharp Specification nameSpecification = s => s .LengthBetween(3, 10).WithMessage("Allowed length: {min}-{max}"); var nameValidator = Validator.Factory.Create(nameSpecification); ``` Initialized with the `nameSpecification` above, `nameValidator` contains the final form of the error message (`"Allowed length: 3 to 10"`) already generated and cached. Performance-wise this is a great approach; still, it also implicates a severe drawback (which, by the way, is the greatest sacrifice I made while designing Validot's internals) - the messages are fully deterministic. So, e.g., you can't include the analyzed value in the error message's content, simply because it's unknown at the time of message generation. Eventually, this could be implemented in the succeeding steps, but is certainly not coming in an initial couple of releases. The messages being pre-generated also make scenarios that include logic branches virtually impossible. Let's take a look at this code using FluentValidation: ``` csharp // part of the FluentValidation's custom validator: RuleFor(x => x.Name).Custom((name, context) => { if (name.Length > 10) { context.AddFailure("Max length is 10"); } else if (name.Length < 4) { context.AddFailure("Min length is 4"); } }); ``` In Validot, the logic is wrapped by the predicates that answer the simple question: is the value valid or not. You don't even get a context where you can check state, assign errors, or decide during validation, e.g., which message will end up in the final report. If asked to replicate the same flow using Validot, I would do this: ``` csharp // part of the Validot's specification: .Member(m => m.Name, m => m .Rule(name => name.Length > 10).WithMessage("Max length is 10") .Rule(name => name.Length < 4).WithMessage("Min length is 4") ) ``` Yet it is rather an exception to the rule, as it might be impracticable to handle even slightly more complicated cases. It's safe to consider Validot as the library of a somewhat different philosophy. Therefore - although it helps achieve similar goals - it isn't a full, one-to-one replacement to FluentValidation. Now, after the validator is initialized, the phase of specification analysis is over. It's a one-time operation with an outcome that can be reused for all upcoming validations because the validator itself is immutable - once created, its internal state can't change. This simple fact also implicates a positive side-effect: more safety in a multi-thread environment. ## Validating objects The second layer of the Validot cake covers error detection. Each `Validate` call creates a validation context that has access to the scheme. With this information, the context traverses through the object (using member selectors from the specification), executes the validation logic (using predicates from the specification), and assembles errors (already pre-generated, also from the specification), linked to their location on the model's map. Obviously, it's an individual process for each validated object, and as an action potentially executed zillion times during the validator's lifetime - it's governed by a completely different set of rules. Principle number one: reduce the amount of work to the bare minimum. The validation context mustn't do anything redundant, so every operation that doesn't change the outcome is entirely skipped. It's easier to present the idea with an example: ``` csharp Specification nameSpecification = s => s .Rule(predicate1).WithMessage("Error1") .Rule(predicate2).WithMessage("Error2") .Rule(predicate3).WithMessage("Error3"); Specification authorSpecification = s => s .Member(m => m.FirstName, nameSpecification) .Member(m => m.LastName, nameSpecification).WithMessage("Invalid last name") var authorValidator = Validator.Factory.Create(authorSpecification); ``` In the snippet above, `nameSpecification` wraps three rules, and it's used within `authorSpecification` to describe the valid state of `FirstName` and `LastName` members. During the validation process, the context enters `FirstName` scope and verifies its value against all rules in `authorSpecification`. However, the situation is different in the `LastName` scope. According to the scheme, its entire error output gets overridden (`WithMessage` command does it). The context knows this fact and avoids unnecessary work by executing the validation logic only until the first discrepancy. Ultimately, it doesn't matter how many and what kind of errors `nameSpecification` indicates. For `LastName` member, the outcome is always the same - `"Invalid last name"`. Principle number two: avoid allocations. Sounds bold, so let's break it down. The fruit of the context's labor is a collection of detected errors assigned to the paths. According to the rule, the collection itself is not even initialized until the first error is found. At first sight, it might look like a micro-optimization, but this operation's scale could be massive. Imagine a validator registered as a singleton within the internal microservice that receives a million calls per day. A collection would be created for each request, even though 99.9% of the requests are correct. Sure, it comes at the price of nulls flying around plus null-checks everywhere, which obviously don't look good. But hey, how often do you debug a nuget package's source code? I consider it a tolerable problem for Validot's maintainers, while its users won't even notice it. What they will notice, though, is the significant performance boost comparing to the process that allocates objects never to be used. Additionally, the validation context's final collection doesn't contain the errors' full content, but merely their integer identifiers. It does make more sense when you consider this example: ``` csharp Specification ageSpecification = s => s .Rule(predicate1).WithMessage("Error 1").WithExtraCode("ERR_1") .Rule(predicate2).WithMessage("Error 2").WithExtraCode("ERR_2") .Rule(predicate3).WithMessage("Error 3").WithExtraCode("ERR_3"); Specification authorSpecification = s => s .Member(m => m.CurrentAge, ageSpecification) .Member(m => m.DeclaredAge, ageSpecification) .Member(m => m.BecomingAuthorAge, ageSpecification) ``` Above, `ageSpecification` holds three rules with a potentially rich output containing codes and messages. Also, `authorSpecification` uses it for its three members. If the context were creating a full set of string messages at this stage, it would unquestionably end up duplicating a lot of data. Using identifiers, it can operate on a simple list of integers, and the final errors' content can easily be resolved later, on-demand. Principle number three: to satisfy the previous two, the code can freely discard the SOLID principles. The most prominent example of such a weird postulate is at the end of the second phase when the validation context finishes its job. The information about detected errors is in the form of `Dictionary>` (error identifiers, mapped the path where they occurred). Indeed more SOLIDish would be passing `IReadOnlyDictionary>` to the next Validot cake layer. Unfortunately, in the real world, that could mean casting and eventually - allocation. Hence, as long as the unSOLID code lives in Validot internals and doesn't leak to the public space, such style is justified. ## Serving results The cake's final layer is about wrapping the validation context's output into an object that allows a quick look-up of the errors found. This tier shares the principles with the preceding one (long story short: don't do unnecessary work, don't allocate prematurely). Yet, this entire process can be considered a separate flow that starts when the validator creates the result of `Validate` method. `IValidationResult` implementation has access to the scheme and the detected error collection (so the products of, respectively, the first and second phases). Unhappily, that makes the result object strongly coupled with the validator, and as such, it's not recommended to pass it outside of the scope of its creation. Alternatively, you should acquire the information you want and let the GC act immediately. Moving the `IValidationResult` object between your app's domains - or worse, caching it in any form - could make your code vulnerable to memory leaks. It is another sacrifice made on the altar of the performance gods. Why? The example scenario is in the [first section](#a-three-layer-cake), so let's analyze something more advanced: ``` csharp // validate input var validationResult = validator.Validate(input); // if no errors, return success immediately if (!validationResult.AnyErrors) { return new SuccessResult(); } // on EMAIL_ERROR code recorded, return appropriate result if (validationResult.Codes.Contains("EMAIL_ERROR")) { return new EmailErrorResult(input.Email); } // log the validation errors (the default language is English) Logger.LogError(validationResult.ToString()); string userLanguage = GetCurrentUserLanguageName(); // create message for the frontend return new ValidationFailureResult(validationResult.ToString(userLanguage)); ``` As stated earlier, returning `validationResult` from the method could be risky, although using its members completely safe. Certainly, this strategy is not that intuitive (and perhaps inconvenient for some), but the reason is straightforward - the performance Validot can reach. Let me explain: the result object delivers the final values on demand, using a lazy manner. The members are initialized and populated only during the first call (or access). It does mean that all potentially expensive operations, e.g., extracting all the error codes or acquiring messages in a particular language, are not performed until the user explicitly wants it. Another thing that sounds great in theory, but the trade-off here is that it works only if the `validationResult` has access to the validator's internals all the time. ## Little optimizations that make a big difference In addition to the principles described in the above sections, Validot is interwoven with little micro-optimizations - both local and spreading across all the three cake layers. Many of them are tailored for the specific use case, like `IsValid` method, but there will be a separate article about it. Another one undoubtedly worth noticing is the success result being cached and shared for all the validations that report correctness. What's the reason behind it? Well, once again, imagine having a microservice that uses Validot for the payloads incoming through its REST API. It's also a part of the internal network, so you can expect that most of the incoming calls will be correct. It happens that the result objects are immutable-ish (the core state doesn't change, but the values are lazy-loaded), so the one without errors is common and could quickly be returned in all such situations. What's more - the validation context doesn't even create the error collection if none detected. Consequently, for a valid model, Validot allocates only a single object - the validation context itself. Naturally, it also depends on the rules' logic, but overall, as a library, Validot is extremely careful when it comes to the managed heap utilization. The [benchmarks below](#validot-vs-fluentvalidation) shows that this approach paid off. In the case when your microservice is public and - let's say - as much as 60% of the incoming traffic is faulty, even then Validot could be 2.5x faster (while consuming 8x less memory) than [FluentValidation](https://fluentvalidation.net/){:target="_blank"}. Results are even better in a not-that-impossible case of all traffic being correct. ## Validot vs FluentValidation [FluentValidation](https://fluentvalidation.net/){:target="_blank"} by [Jeremy Skinner](https://twitter.com/JeremySkinner){:target="_blank"} is the gold standard in the dotnet world and a great, reliable, opensource, battle-tested library that is around for years. I don't believe the one could find a better reference point for the validation benchmarks. For these test runs, I created a model containing all kinds of members. They are grouped into three sets, each containing 10k objects - the first one has errors in all items, the second one - in circa 60% of them, and the third one is error-free. Validot's specification and FluentValidation's custom validator reflect each other as much as technically possible. And this is how `Validate` performs in both libs: | Set | Library | Mean [ms] | Allocated [MB] | | - | - | -: | -: | | 1st (100% errors) | FluentValidation | `774.69` | `747.66` | | 1st (100% errors) | Validot | `331.70` | `183.19` | | 2nd (~60% errors) | FluentValidation | `711.15` | `675.69` | | 2nd (~60% errors) | Validot | `271.77` | `85.10` | | 3rd (0% errors) | FluentValidation | `659.07` | `660.00` | | 3rd (0% errors) | Validot | `242.92` | `78.82 ` | _Benchmarks environment: Validot 1.1.0, FluentValidation 9.2.0, .NET Core 3.1.7, i7-9750H (2.60GHz, 1 CPU, 12 logical and 6 physical cores), X64 RyuJIT, macOS Catalina, BenchmarkDotNet 0.12.1. You are very welcome to [run the benchmarks yourself](https://github.com/bartoszlenar/Validot/blob/main/docs/DOCUMENTATION.md#benchmarks){:target="_blank"} and [review their code](https://github.com/bartoszlenar/Validot/tree/main/tests/Validot.Benchmarks){:target="_blank"}_ Please bear in mind that although Validot could be more performant, the trade-offs are limitations, different API, philosophy of work, and eventually - a smaller range of possibilities. Some scenarios are not possible yet - others never will be. True, Validot can't be considered a 100% replacement to FluentValidation (it's more like 90%), but I firmly believe it can handle most validation cases, including highly complex ones. ## Afterword Validot is an open-source, MIT-licenced, fully tested, and documented project, hosted entirely on [github](https://github.com/bartoszlenar/Validot){:target="_blank"}. Type ``` dotnet add package Validot ``` and give it a try in your next dotnet-based microservice. ================================================ FILE: nuget.config ================================================ ================================================ FILE: src/Validot/CodeHelper.cs ================================================ namespace Validot { using System.Linq; internal static class CodeHelper { public static bool IsCodeValid(string code) { if (string.IsNullOrEmpty(code)) { return false; } return code.All(c => !char.IsWhiteSpace(c)); } } } ================================================ FILE: src/Validot/Errors/Args/ArgHelper.cs ================================================ namespace Validot.Errors.Args { using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; internal static class ArgHelper { private static readonly Regex CurlyBracketsRegex = new Regex(@"(?<=\{)[^}]*(?=\})", RegexOptions.Compiled); private static readonly IReadOnlyDictionary EmptyParametersDictionary = new Dictionary(); public static char Divider { get; } = '|'; public static char Assignment { get; } = '='; public static string FormatMessage(string message, IReadOnlyList placeholders, IReadOnlyList args) { if (message == null) { return string.Empty; } if (args == null || !args.Any()) { return message; } if (placeholders == null || !placeholders.Any()) { return message; } var messageBuilder = new StringBuilder(message); foreach (ArgPlaceholder placeholder in placeholders) { IArg arg = args.SingleOrDefault(a => a.Name == placeholder.Name); if (arg == null) { continue; } var withInvalidParameters = placeholder.Parameters != null && placeholder.Parameters.Count > 0 && placeholder.Parameters.Keys.Any(param => !arg.AllowedParameters.Contains(param)); if (withInvalidParameters) { continue; } var value = arg.ToString(placeholder.Parameters); _ = messageBuilder.Replace(placeholder.Placeholder, value); } return messageBuilder.ToString(); } public static IReadOnlyList ExtractPlaceholders(string message) { ThrowHelper.NullArgument(message, nameof(message)); string[] matches = CurlyBracketsRegex.Matches(message) .Cast() .Select(m => m.Value) .Distinct() .ToArray(); var placeholders = new List(matches.Length); foreach (var match in matches) { string[] parts = match.Split(Divider); var name = parts.FirstOrDefault(); if (string.IsNullOrWhiteSpace(name)) { continue; } var placeholder = $"{{{match}}}"; if (parts.Length == 1) { placeholders.Add(new ArgPlaceholder { Placeholder = placeholder, Name = name, Parameters = EmptyParametersDictionary }); } else { Dictionary parameters = null; var invalidPart = false; for (var i = 1; i < parts.Length; ++i) { var item = parts.ElementAt(i); if (!item.Contains(Assignment)) { invalidPart = true; break; } string[] groups = item.Split(Assignment).Where(p => !string.IsNullOrWhiteSpace(p)).ToArray(); if (groups.Length != 2) { invalidPart = true; break; } if (parameters == null) { parameters = new Dictionary(); } if (parameters.ContainsKey(groups.ElementAt(0))) { invalidPart = true; break; } parameters.Add(groups.ElementAt(0), groups.ElementAt(1)); } if (invalidPart) { continue; } placeholders.Add(new ArgPlaceholder { Placeholder = placeholder, Name = name, Parameters = parameters ?? EmptyParametersDictionary }); } } return placeholders; } } } ================================================ FILE: src/Validot/Errors/Args/ArgPlaceholder.cs ================================================ namespace Validot.Errors.Args { using System.Collections.Generic; public sealed class ArgPlaceholder { public string Placeholder { get; set; } public string Name { get; set; } public IReadOnlyDictionary Parameters { get; set; } } } ================================================ FILE: src/Validot/Errors/Args/EnumArg.cs ================================================ namespace Validot.Errors.Args { using System; using System.Collections.Generic; public sealed class EnumArg : IArg where T : struct { private const string TranslationParameter = "translation"; private const string TranslationParameterValue = "true"; private const string FormatParameter = "format"; private const string DefaultFormat = "G"; public EnumArg(string name, T value) { ThrowHelper.NullArgument(name, nameof(name)); Name = name; Value = value; } public string Name { get; } public T Value { get; } public IReadOnlyCollection AllowedParameters { get; } = new[] { FormatParameter, TranslationParameter }; public string ToString(IReadOnlyDictionary parameters) { if (parameters?.ContainsKey(TranslationParameter) == true && parameters[TranslationParameter] == TranslationParameterValue) { var key = Enum.Format(typeof(T), Value, "f"); return TranslationArg.CreatePlaceholder($"Enum.{typeof(T).FullName}.{key}"); } var format = parameters?.ContainsKey(FormatParameter) == true ? parameters[FormatParameter] : DefaultFormat; return Enum.Format(typeof(T), Value, format); } } } ================================================ FILE: src/Validot/Errors/Args/EnumArgFactory.cs ================================================ namespace Validot { using Validot.Errors.Args; public static partial class Arg { public static IArg Enum(string name, T value) where T : struct { return new EnumArg(name, value); } } } ================================================ FILE: src/Validot/Errors/Args/GuidArg.cs ================================================ namespace Validot.Errors.Args { using System; using System.Collections.Generic; using System.Globalization; public sealed class GuidArg : IArg { private const string FormatParameter = "format"; private const string DefaultFormat = "D"; private const string CaseParameter = "case"; private const string UpperCaseParameterValue = "upper"; private const string LowerCaseParameterValue = "lower"; public GuidArg(string name, Guid value) { ThrowHelper.NullArgument(name, nameof(name)); Name = name; Value = value; } public string Name { get; } public Guid Value { get; } public IReadOnlyCollection AllowedParameters { get; } = new[] { FormatParameter, CaseParameter }; public string ToString(IReadOnlyDictionary parameters) { var caseParameter = parameters?.ContainsKey(CaseParameter) == true ? parameters[CaseParameter] : null; if (caseParameter != null && caseParameter != UpperCaseParameterValue && caseParameter != LowerCaseParameterValue) { caseParameter = null; } var format = parameters?.ContainsKey(FormatParameter) == true ? parameters[FormatParameter] : null; if (format == null) { format = DefaultFormat; } var stringifiedGuid = Value.ToString(format, CultureInfo.InvariantCulture); if (caseParameter == UpperCaseParameterValue) { return stringifiedGuid.ToUpper(CultureInfo.InvariantCulture); } if (caseParameter == LowerCaseParameterValue) { return stringifiedGuid.ToLower(CultureInfo.InvariantCulture); } return stringifiedGuid; } } } ================================================ FILE: src/Validot/Errors/Args/GuidArgFactory.cs ================================================ namespace Validot { using System; using Validot.Errors.Args; public static partial class Arg { public static IArg GuidValue(string name, Guid value) { return new GuidArg(name, value); } } } ================================================ FILE: src/Validot/Errors/Args/IArg.cs ================================================ namespace Validot.Errors.Args { using System.Collections.Generic; public interface IArg { string Name { get; } IReadOnlyCollection AllowedParameters { get; } string ToString(IReadOnlyDictionary parameters); } public interface IArg : IArg { T Value { get; } } } ================================================ FILE: src/Validot/Errors/Args/NameArg.cs ================================================ namespace Validot.Errors.Args { using System; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; public sealed class NameArg : IArg { private const string FormatParameter = "format"; private const string TitleCaseParameterValue = "titleCase"; private static readonly string[] KeyAsAllowedParameters = { FormatParameter }; private static readonly IReadOnlyList _titleCaseRegexes = new[] { new Regex(@"([a-z])([A-Z][a-z])", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([A-Z][a-z])([A-Z])", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([a-z])([A-Z]+[a-z])", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([A-Z]+)([A-Z][a-z][a-z])", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([a-z]+)([A-Z0-9]+)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([A-Z]+)([A-Z][a-rt-z][a-z]*)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([0-9])([A-Z][a-z]+)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([A-Z]{2,})([0-9]{2,})", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), new Regex(@"([0-9]{2,})([A-Z]{2,})", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)), }; private readonly string _name; public static string Name { get; } = "_name"; string IArg.Name => Name; public IReadOnlyCollection AllowedParameters => KeyAsAllowedParameters; public string ToString(IReadOnlyDictionary parameters) { var formatParameter = parameters?.ContainsKey(FormatParameter) == true ? parameters[FormatParameter] : null; if (formatParameter == null) { return _name; } return Stringify(_name, formatParameter); } public NameArg(string name) { ThrowHelper.NullArgument(name, nameof(name)); _name = name; } private static string Stringify(string value, string formatParameter) { if (string.Equals(formatParameter, TitleCaseParameterValue, StringComparison.InvariantCulture)) { return ConvertToTitleCase(value); } return value; } // Title case method taken from https://stackoverflow.com/a/35953318/1633913 private static string ConvertToTitleCase(string input) { if (input.Contains("_")) { input = input.Replace('_', ' '); input = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(input); } foreach (var regex in _titleCaseRegexes) { input = regex.Replace(input, "$1 $2"); } input = input.Trim(); if (input.Length == 1) { return char.ToUpperInvariant(input[0]).ToString(CultureInfo.InvariantCulture); } return char.ToUpperInvariant(input[0]).ToString(CultureInfo.InvariantCulture) + input.Substring(1); } } } ================================================ FILE: src/Validot/Errors/Args/NumberArg.cs ================================================ namespace Validot.Errors.Args { using System; using System.Collections.Generic; using System.Globalization; public sealed class NumberArg : NumberArg, IArg { private readonly Func _stringify; public NumberArg(string name, T value, Func stringify) { ThrowHelper.NullArgument(name, nameof(name)); ThrowHelper.NullArgument(stringify, nameof(stringify)); Name = name; Value = value; _stringify = stringify; } public T Value { get; } public string Name { get; } public IReadOnlyCollection AllowedParameters { get; } = new[] { FormatParameter, CultureParameter }; public override string ToString(IReadOnlyDictionary parameters) { var format = parameters?.ContainsKey(FormatParameter) == true ? parameters[FormatParameter] : null; var culture = parameters?.ContainsKey(CultureParameter) == true ? CultureInfo.GetCultureInfo(parameters[CultureParameter]) : null; if (format == null && culture == null) { format = DefaultFormat; culture = DefaultCultureInfo; } else if (format != null && culture == null) { culture = DefaultCultureInfo; } return _stringify(Value, format, culture); } } public abstract class NumberArg { protected static string FormatParameter => "format"; protected static string CultureParameter => "culture"; protected CultureInfo DefaultCultureInfo { get; } = CultureInfo.InvariantCulture; protected string DefaultFormat { get; } = string.Empty; public abstract string ToString(IReadOnlyDictionary parameters); } } ================================================ FILE: src/Validot/Errors/Args/NumberArgFactory.cs ================================================ namespace Validot { using Validot.Errors.Args; public static partial class Arg { public static IArg Number(string name, int value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, uint value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, float value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, double value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, decimal value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, byte value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, sbyte value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, long value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, ulong value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, short value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } public static IArg Number(string name, ushort value) { return new NumberArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)); } } } ================================================ FILE: src/Validot/Errors/Args/TextArg.cs ================================================ namespace Validot.Errors.Args { using System.Collections.Generic; using System.Globalization; public sealed class TextArg : IArg { private const string CaseParameter = "case"; private const string UpperCaseParameterValue = "upper"; private const string LowerCaseParameterValue = "lower"; private static readonly string[] StaticAllowedParameters = { CaseParameter }; public TextArg(string name, string value) { ThrowHelper.NullArgument(name, nameof(name)); ThrowHelper.NullArgument(value, nameof(value)); Value = value; Name = name; } public TextArg(string name, char value) : this(name, value.ToString(CultureInfo.InvariantCulture)) { } public string Name { get; } public string Value { get; } public IReadOnlyCollection AllowedParameters => StaticAllowedParameters; public string ToString(IReadOnlyDictionary parameters) { var caseParameter = parameters?.ContainsKey(CaseParameter) == true ? parameters[CaseParameter] : null; if (caseParameter != null && caseParameter != UpperCaseParameterValue && caseParameter != LowerCaseParameterValue) { caseParameter = null; } return Stringify(Value, caseParameter); } private static string Stringify(string value, string caseParameter) { if (caseParameter == UpperCaseParameterValue) { return value.ToUpper(CultureInfo.InvariantCulture); } if (caseParameter == LowerCaseParameterValue) { return value.ToLower(CultureInfo.InvariantCulture); } return value; } } } ================================================ FILE: src/Validot/Errors/Args/TextArgFactory.cs ================================================ namespace Validot { using Validot.Errors.Args; public static partial class Arg { public static IArg Text(string name, string value) { return new TextArg(name, value); } public static IArg Text(string name, char value) { return new TextArg(name, value); } } } ================================================ FILE: src/Validot/Errors/Args/TimeArg.cs ================================================ namespace Validot.Errors.Args { using System; using System.Collections.Generic; using System.Globalization; public sealed class TimeArg : TimeArg, IArg { private readonly Func _stringify; public TimeArg(string name, T value, Func stringify) { ThrowHelper.NullArgument(name, nameof(name)); ThrowHelper.NullArgument(stringify, nameof(stringify)); Name = name; Value = value; _stringify = stringify; } public T Value { get; } public string Name { get; } public IReadOnlyCollection AllowedParameters { get; } = new[] { FormatParameter, CultureParameter }; public override string ToString(IReadOnlyDictionary parameters) { var format = parameters?.ContainsKey(FormatParameter) == true ? parameters[FormatParameter] : null; var culture = parameters?.ContainsKey(CultureParameter) == true ? CultureInfo.GetCultureInfo(parameters[CultureParameter]) : null; if (format == null && culture == null) { format = DefaultFormat; culture = DefaultCultureInfo; } else if (format != null && culture == null) { culture = DefaultCultureInfo; } return _stringify(Value, format, culture); } } public abstract class TimeArg { internal string DefaultFormat { get; set; } = string.Empty; protected static string FormatParameter => "format"; protected static string CultureParameter => "culture"; protected static CultureInfo DefaultCultureInfo { get; } = CultureInfo.InvariantCulture; public abstract string ToString(IReadOnlyDictionary parameters); } } ================================================ FILE: src/Validot/Errors/Args/TimeArgFactory.cs ================================================ namespace Validot { using System; using Validot.Errors.Args; public static partial class Arg { public static IArg Time(string name, DateTime value) { return new TimeArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)) { DefaultFormat = DateTimeFormats.DateAndTimeFormat }; } public static IArg Time(string name, DateTimeOffset value) { return new TimeArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)) { DefaultFormat = DateTimeFormats.DateAndTimeFormat }; } public static IArg Time(string name, TimeSpan value) { return new TimeArg(name, value, (v, format, cultureInfo) => v.ToString(format, cultureInfo)) { DefaultFormat = DateTimeFormats.TimeSpanFormat }; } } } ================================================ FILE: src/Validot/Errors/Args/TranslationArg.cs ================================================ namespace Validot.Errors.Args { using System.Collections.Generic; public sealed class TranslationArg : IArg { private const string KeyParameter = "key"; private static readonly string[] KeyAsAllowedParameters = { KeyParameter }; private readonly IReadOnlyDictionary _translation; public TranslationArg(IReadOnlyDictionary translation) { ThrowHelper.NullArgument(translation, nameof(translation)); _translation = translation; } public static string Name { get; } = "_translation"; string IArg.Name { get; } = Name; public IReadOnlyCollection AllowedParameters => KeyAsAllowedParameters; public static string CreatePlaceholder(string key) { return $"{{{Name}|key={key}}}"; } public string ToString(IReadOnlyDictionary parameters) { var keyParameter = parameters?.ContainsKey(KeyParameter) == true ? parameters[KeyParameter] : null; if (keyParameter == null) { return Name; } return _translation.ContainsKey(keyParameter) ? _translation[keyParameter] : keyParameter; } } } ================================================ FILE: src/Validot/Errors/Args/TypeArg.cs ================================================ namespace Validot.Errors.Args { using System; using System.Collections.Generic; public sealed class TypeArg : IArg { private const string TranslationParameter = "translation"; private const string TranslationParameterValue = "true"; private const string FormatParameter = "format"; private const string NameFormat = "name"; private const string ToStringFormat = "toString"; private const string FullNameFormat = "fullName"; private const string DefaultFormat = NameFormat; public TypeArg(string name, Type value) { ThrowHelper.NullArgument(name, nameof(name)); Name = name; Value = value; } public string Name { get; } public Type Value { get; } public IReadOnlyCollection AllowedParameters { get; } = new[] { TranslationParameter, FormatParameter }; public string ToString(IReadOnlyDictionary parameters) { if (parameters?.ContainsKey(TranslationParameter) == true && parameters[TranslationParameter] == TranslationParameterValue) { return $"{{_translation|key=Type.{Value.GetFriendlyName(true)}}}"; } var format = parameters?.ContainsKey(FormatParameter) == true ? parameters[FormatParameter] : DefaultFormat; if (format == ToStringFormat) { return Value.ToString(); } if (format == FullNameFormat) { return Value.GetFriendlyName(true); } return Value.GetFriendlyName(format == FullNameFormat); } } } ================================================ FILE: src/Validot/Errors/Args/TypeArgFactory.cs ================================================ namespace Validot { using System; using Validot.Errors.Args; public static partial class Arg { public static IArg Type(string name, Type value) { return new TypeArg(name, value); } } } ================================================ FILE: src/Validot/Errors/CacheIntegrityException.cs ================================================ namespace Validot.Errors { public sealed class CacheIntegrityException : ValidotException { public CacheIntegrityException(string message) : base(message) { } } } ================================================ FILE: src/Validot/Errors/Error.cs ================================================ namespace Validot.Errors { using System.Collections.Generic; using Validot.Errors.Args; internal class Error : IError { public IReadOnlyList Messages { get; set; } public IReadOnlyList Codes { get; set; } public IReadOnlyList Args { get; set; } } } ================================================ FILE: src/Validot/Errors/IError.cs ================================================ namespace Validot.Errors { using System.Collections.Generic; using Validot.Errors.Args; public interface IError { IReadOnlyList Messages { get; } IReadOnlyList Codes { get; } IReadOnlyList Args { get; } } } ================================================ FILE: src/Validot/Errors/IMessageService.cs ================================================ namespace Validot.Errors { using System.Collections.Generic; internal interface IMessageService { IReadOnlyList TranslationNames { get; } IReadOnlyDictionary GetTranslation(string translationName); IReadOnlyDictionary> GetMessages(Dictionary> errors, string translationName = null); } } ================================================ FILE: src/Validot/Errors/MessageCache.cs ================================================ namespace Validot.Errors { using System; using System.Collections.Generic; using System.Linq; using Validot.Errors.Args; internal class MessageCache { private readonly Dictionary>> _messages = new Dictionary>>(); private readonly Dictionary _messagesAmount = new Dictionary(); private readonly Dictionary>>> _messagesWithPathArgs = new Dictionary>>>(); private readonly Dictionary>>> _placeholders = new Dictionary>>>(); public void AddMessage(string translationName, int errorId, IReadOnlyList messages) { ThrowHelper.NullArgument(translationName, nameof(translationName)); ThrowHelper.NullInCollection(messages, nameof(messages)); if (!_messages.ContainsKey(translationName)) { _messages.Add(translationName, new Dictionary>()); } _messages[translationName].Add(errorId, messages); if (!_messagesAmount.ContainsKey(errorId)) { _messagesAmount.Add(errorId, messages.Count); } } public void AddIndexedPathPlaceholders(string translationName, int errorId, IReadOnlyDictionary> indexedPlaceholders) { ThrowHelper.NullArgument(translationName, nameof(translationName)); ThrowHelper.NullArgument(indexedPlaceholders, nameof(indexedPlaceholders)); foreach (var pair in indexedPlaceholders) { ThrowHelper.NullInCollection(pair.Value, $"{nameof(indexedPlaceholders)}[{pair.Key}]"); if (pair.Value.Any(IsNullInArgPlaceholder)) { throw new ArgumentNullException(nameof(indexedPlaceholders), $"Null in {nameof(ArgPlaceholder)}"); } } if (!_placeholders.ContainsKey(translationName)) { _placeholders.Add(translationName, new Dictionary>>()); } _placeholders[translationName].Add(errorId, indexedPlaceholders); } public void AddMessageWithPathArgs(string translationName, string path, int errorId, IReadOnlyList messages) { ThrowHelper.NullArgument(translationName, nameof(translationName)); ThrowHelper.NullArgument(path, nameof(path)); ThrowHelper.NullInCollection(messages, nameof(messages)); if (!_messagesWithPathArgs.ContainsKey(translationName)) { _messagesWithPathArgs.Add(translationName, new Dictionary>>()); } if (!_messagesWithPathArgs[translationName].ContainsKey(path)) { _messagesWithPathArgs[translationName].Add(path, new Dictionary>()); } _messagesWithPathArgs[translationName][path].Add(errorId, messages); } public int GetMessageAmount(List errorsIds) { var amount = 0; for (var i = 0; i < errorsIds.Count; ++i) { amount += _messagesAmount[errorsIds[i]]; } return amount; } public IReadOnlyList GetMessages(string translationName, int errorId) { return _messages[translationName][errorId]; } public IReadOnlyDictionary> GetIndexedPathPlaceholders(string translationName, int errorId) { return _placeholders[translationName][errorId]; } public IReadOnlyList GetMessagesWithPathArgs(string translationName, string path, int errorId) { return _messagesWithPathArgs[translationName][path][errorId]; } public bool IsMessageWithPathArgsCached(string translationName, string path, int errorId) { return _messagesWithPathArgs.ContainsKey(translationName) && _messagesWithPathArgs[translationName].ContainsKey(path) && _messagesWithPathArgs[translationName][path].ContainsKey(errorId); } public bool ContainsPathArgs(string translationName, int errorId) { return _placeholders.ContainsKey(translationName) && _placeholders[translationName].ContainsKey(errorId); } public void VerifyIntegrity() { var allErrorsIds = _messages.Any() ? _messages.FirstOrDefault().Value.Keys.ToArray() : Array.Empty(); foreach (var pair in _messages) { var translation = pair.Key; foreach (var errorPair in pair.Value) { var errorId = errorPair.Key; if (!allErrorsIds.Contains(errorId)) { throw new CacheIntegrityException($"ErrorId {errorId} is not present in all translations"); } var errorMessages = errorPair.Value; if (errorMessages.Count != _messagesAmount[errorId]) { throw new CacheIntegrityException($"ErrorId {errorId}, messages amount is expected to be {_messagesAmount[errorId]} but found {errorMessages.Count} in translation `{translation}`"); } } } foreach (var pair in _placeholders) { var translation = pair.Key; if (!_messages.ContainsKey(translation)) { throw new CacheIntegrityException($"Translation `{translation}` is not expected in path placeholders"); } foreach (var pair2 in pair.Value) { var errorId = pair2.Key; if (!allErrorsIds.Contains(errorId)) { throw new CacheIntegrityException($"ErrorId {errorId} is not expected in path placeholders (translation `{translation}`)"); } var indexes = pair2.Value.Keys; var maxIndex = _messagesAmount[errorId] - 1; if (indexes.Any(i => i > maxIndex)) { var aboveMax = indexes.First(i => i > maxIndex); throw new CacheIntegrityException($"ErrorId {errorId} max index for path placeholder is {maxIndex}, but found {aboveMax} (translation `{translation}`)"); } } } foreach (var pair in _messagesWithPathArgs) { var translation = pair.Key; if (!_messages.ContainsKey(translation)) { throw new CacheIntegrityException($"Translation `{translation}` is not expected in messages with path args"); } foreach (var pair2 in pair.Value) { var path = pair2.Key; foreach (var pair3 in pair2.Value) { var errorId = pair3.Key; if (!allErrorsIds.Contains(errorId)) { throw new CacheIntegrityException($"Error ID {errorId} in translation `{translation}` is not expected in messages with path args"); } if (pair3.Value.Count > _messagesAmount[errorId]) { throw new CacheIntegrityException($"Error ID {errorId} is expected to have max {_messagesAmount[errorId]} messages, but found {pair3.Value.Count} in messages with path args (for translation `{translation}` and path `{path}`)"); } } } } } private bool IsNullInArgPlaceholder(ArgPlaceholder argPlaceholder) { return argPlaceholder.Name == null || argPlaceholder.Placeholder == null || argPlaceholder.Parameters == null || (argPlaceholder.Parameters.Count > 0 && argPlaceholder.Parameters.Values.Any(p => p is null)); } } } ================================================ FILE: src/Validot/Errors/MessageService.cs ================================================ namespace Validot.Errors { using System.Collections.Generic; using System.Linq; using Validot.Errors.Translator; using Validot.Translations; internal class MessageService : IMessageService { private readonly MessageCache _cache; private readonly MessageTranslator _translator; public MessageService( IReadOnlyDictionary> translations, IReadOnlyDictionary errors, IReadOnlyDictionary> template) { _translator = new MessageTranslator(translations); _cache = BuildMessageCache(_translator, errors, template); } public IReadOnlyList TranslationNames => _translator.TranslationNames; public IReadOnlyDictionary GetTranslation(string translationName) { return _translator.Translations[translationName]; } public IReadOnlyDictionary> GetMessages(Dictionary> errors, string translationName = null) { var results = new Dictionary>(errors.Count); translationName = translationName ?? nameof(Translation.English); foreach (var pair in errors) { var path = pair.Key; var errorsIds = pair.Value; var capacity = _cache.GetMessageAmount(errorsIds); if (capacity == 0) { continue; } var allMessages = new string[capacity]; var index = 0; for (var i = 0; i < errorsIds.Count; ++i) { var errorId = errorsIds[i]; IReadOnlyList messages; if (!_cache.ContainsPathArgs(translationName, errorId)) { messages = _cache.GetMessages(translationName, errorId); } else if (_cache.IsMessageWithPathArgsCached(translationName, path, errorId)) { messages = _cache.GetMessagesWithPathArgs(translationName, path, errorId); } else { var cachedMessages = _cache.GetMessages(translationName, errorId); var indexedPathPlaceholders = _cache.GetIndexedPathPlaceholders(translationName, errorId); messages = MessageTranslator.TranslateMessagesWithPathPlaceholders(path, cachedMessages, indexedPathPlaceholders); } CopyMessages(messages, allMessages, ref index); } results.Add(path, allMessages); } return results; } private void CopyMessages(IReadOnlyList source, string[] target, ref int targetIndex) { for (var i = 0; i < source.Count; ++i) { target[targetIndex + i] = source[i]; } targetIndex += source.Count; } private MessageCache BuildMessageCache(MessageTranslator translator, IReadOnlyDictionary errors, IReadOnlyDictionary> template) { ThrowHelper.NullArgument(errors, nameof(errors)); ThrowHelper.NullArgument(template, nameof(template)); ThrowHelper.NullInCollection(template.Values.ToArray(), $"{nameof(template)}.{nameof(template.Values)}"); var uniqueErrorsIds = template.SelectMany(b => b.Value).Distinct().ToArray(); var cache = new MessageCache(); foreach (var translationName in TranslationNames) { foreach (var errorId in uniqueErrorsIds) { var translationResult = translator.TranslateMessages(translationName, errors[errorId]); cache.AddMessage(translationName, errorId, translationResult.Messages); if (translationResult.AnyPathPlaceholders) { cache.AddIndexedPathPlaceholders(translationName, errorId, translationResult.IndexedPathPlaceholders); } } } foreach (var translationName in TranslationNames) { foreach (var templatePair in template) { var path = templatePair.Key; foreach (var errorId in templatePair.Value) { if (!cache.ContainsPathArgs(translationName, errorId) || PathHelper.ContainsIndexes(path)) { continue; } var cachedMessages = cache.GetMessages(translationName, errorId); var indexedPlaceholders = cache.GetIndexedPathPlaceholders(translationName, errorId); var errorMessagesWithSpecials = MessageTranslator.TranslateMessagesWithPathPlaceholders(path, cachedMessages, indexedPlaceholders); cache.AddMessageWithPathArgs(translationName, path, errorId, errorMessagesWithSpecials); } } } return cache; } } } ================================================ FILE: src/Validot/Errors/ReferenceLoopError.cs ================================================ namespace Validot.Errors { using System; using System.Collections.Generic; using Validot.Errors.Args; using Validot.Translations; internal class ReferenceLoopError : IError { public ReferenceLoopError(Type type) { Messages = new[] { MessageKey.Global.ReferenceLoop }; Args = new[] { Arg.Type("type", type) }; Codes = Array.Empty(); } public IReadOnlyList Messages { get; } public IReadOnlyList Codes { get; } public IReadOnlyList Args { get; } } } ================================================ FILE: src/Validot/Errors/Translator/MessageTranslator.cs ================================================ namespace Validot.Errors.Translator { using System.Collections.Generic; using System.Linq; using Validot.Errors.Args; internal class MessageTranslator { private const string NameArgName = "_name"; private const string PathArgName = "_path"; private static readonly IReadOnlyDictionary> _emptyIndexedPathPlaceholders = new Dictionary>(); public MessageTranslator(IReadOnlyDictionary> translations) { ThrowHelper.NullArgument(translations, nameof(translations)); ThrowHelper.NullInCollection(translations.Values.ToArray(), $"{nameof(translations)}.Values"); foreach (var pair in translations) { ThrowHelper.NullInCollection(pair.Value.Values.ToArray(), $"{nameof(translations)}[{pair.Key}].Values"); } Translations = translations; TranslationArgs = BuildTranslationArgs(translations); TranslationNames = translations.Keys.ToArray(); } public IReadOnlyDictionary> Translations { get; } public IReadOnlyList TranslationNames { get; } public IReadOnlyDictionary TranslationArgs { get; } public static IReadOnlyList TranslateMessagesWithPathPlaceholders(string path, IReadOnlyList errorMessages, IReadOnlyDictionary> indexedPathsPlaceholders) { var pathArgs = CreatePathArgsForPath(path); var result = new string[errorMessages.Count]; for (var i = 0; i < errorMessages.Count; ++i) { if (indexedPathsPlaceholders.ContainsKey(i)) { result[i] = ArgHelper.FormatMessage(errorMessages[i], indexedPathsPlaceholders[i], pathArgs); } else { result[i] = errorMessages[i]; } } return result; } public TranslationResult TranslateMessages(string translationName, IError error) { ThrowHelper.NullArgument(error, nameof(error)); ThrowHelper.NullInCollection(error.Messages, $"{nameof(error)}.{nameof(error.Messages)}"); ThrowHelper.NullInCollection(error.Args, $"{nameof(error)}.{nameof(error.Args)}"); var translation = Translations[translationName]; var messages = new string[error.Messages.Count]; Dictionary> indexedPathPlaceholders = null; for (var i = 0; i < error.Messages.Count; ++i) { var key = error.Messages.ElementAt(i); var message = translation.ContainsKey(key) ? translation[key] : key; var placeholders = ArgHelper.ExtractPlaceholders(message); messages[i] = ArgHelper.FormatMessage(message, placeholders, error.Args); if (TryExtractSpecialArgs(translationName, messages[i], out var specialPlaceholders, out var specialArgs)) { messages[i] = ArgHelper.FormatMessage(messages[i], specialPlaceholders, specialArgs); } if (TryExtractPathPlaceholders(messages[i], out var pathPlaceholders)) { if (indexedPathPlaceholders == null) { indexedPathPlaceholders = new Dictionary>(messages.Length - i); } indexedPathPlaceholders.Add(i, pathPlaceholders); } } return new TranslationResult { Messages = messages, IndexedPathPlaceholders = indexedPathPlaceholders ?? _emptyIndexedPathPlaceholders }; } private static IReadOnlyList CreatePathArgsForPath(string path) { var name = PathHelper.GetLastLevel(path); return new[] { Arg.Text(PathArgName, path), new NameArg(name), }; } private static bool TryExtractPathPlaceholders(string message, out ArgPlaceholder[] placeholders) { placeholders = ArgHelper.ExtractPlaceholders(message).Where(p => p.Name == NameArgName || p.Name == PathArgName).ToArray(); return placeholders.Any(); } private bool TryExtractSpecialArgs(string translationName, string message, out IReadOnlyList specialPlaceholders, out IReadOnlyList specialArgs) { specialPlaceholders = ArgHelper.ExtractPlaceholders(message).Where(p => p.Name == TranslationArg.Name).ToArray(); if (specialPlaceholders.Any()) { specialArgs = TranslationArgs[translationName]; return true; } specialArgs = null; return false; } private IReadOnlyDictionary BuildTranslationArgs(IReadOnlyDictionary> translations) { var translationArgs = new Dictionary(translations.Count); foreach (var pair in translations) { var args = new IArg[] { new TranslationArg(pair.Value) }; translationArgs.Add(pair.Key, args); } return translationArgs; } } } ================================================ FILE: src/Validot/Errors/Translator/TranslationResult.cs ================================================ namespace Validot.Errors.Translator { using System.Collections.Generic; using Validot.Errors.Args; internal class TranslationResult { public IReadOnlyList Messages { get; set; } public IReadOnlyDictionary> IndexedPathPlaceholders { get; set; } public bool AnyPathPlaceholders => IndexedPathPlaceholders != null && IndexedPathPlaceholders.Count > 0; } } ================================================ FILE: src/Validot/Factory/HolderInfo.cs ================================================ namespace Validot.Factory { using System; using System.Linq; using Validot.Settings; /// /// Information about the class that implements (and, optionally, ). /// public class HolderInfo { internal HolderInfo(Type holderType, Type specifiedType) { if (holderType is null) { throw new ArgumentNullException(nameof(holderType)); } if (specifiedType is null) { throw new ArgumentNullException(nameof(specifiedType)); } var hasParameterlessConstructor = holderType.IsClass && holderType.GetConstructor(Type.EmptyTypes) != null; if (!hasParameterlessConstructor) { throw new ArgumentException($"{holderType.GetFriendlyName()} must be a class and have parameterless constructor.", nameof(holderType)); } if (holderType.GetInterfaces().All(i => i != typeof(ISpecificationHolder<>).MakeGenericType(specifiedType))) { throw new ArgumentException($"{holderType.GetFriendlyName()} is not a holder for {specifiedType.GetFriendlyName()} specification (doesn't implement ISpecificationHolder<{specifiedType.GetFriendlyName()}>).", nameof(holderType)); } HolderType = holderType; SpecifiedType = specifiedType; HoldsSettings = holderType.GetInterfaces().Any(@interface => @interface == typeof(ISettingsHolder)); ValidatorType = typeof(IValidator<>).MakeGenericType(SpecifiedType); } /// /// Gets the type of the specification holder. /// public Type HolderType { get; } /// /// Gets the type that is covered by the specification. It's T from ISpecificationHolder{T} and its member Specification{T}. /// public Type SpecifiedType { get; } /// /// Gets the type of the validator. It's IValidator{T}, where T is . /// /// public Type ValidatorType { get; } /// /// Gets a value indicating whether the specification holder is also a settings holder (implements ). /// public bool HoldsSettings { get; } /// /// Creates the validator (of type IValidator{T}, where T is ) using the information from specification holder. /// /// IValidator{T} where T is . public object CreateValidator() { var holderInstance = Activator.CreateInstance(HolderType); var holderInterfaceType = typeof(ISpecificationHolder<>).MakeGenericType(SpecifiedType); var specificationType = typeof(Specification<>).MakeGenericType(SpecifiedType); var specificationPropertyInfo = holderInterfaceType.GetProperty(nameof(ISpecificationHolder.Specification), specificationType); var specification = specificationPropertyInfo.GetValue(holderInstance); Func settingsBuilder; if (HoldsSettings) { var settingsPropertyInfo = typeof(ISettingsHolder).GetProperty(nameof(ISettingsHolder.Settings)); settingsBuilder = settingsPropertyInfo?.GetValue(holderInstance) as Func; } else { settingsBuilder = null; } var createArgs = new[] { specification, settingsBuilder }; var createMethodInfo = typeof(ValidatorFactory).GetMethods() .Single(m => m.Name == nameof(ValidatorFactory.Create) && m.IsGenericMethod && m.GetGenericArguments().Length == 1 && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(Specification<>).GetGenericTypeDefinition() && m.GetParameters()[1].ParameterType == typeof(Func)) .MakeGenericMethod(SpecifiedType); var validator = createMethodInfo.Invoke(Validator.Factory, createArgs); return validator; } } } ================================================ FILE: src/Validot/Factory/ISettingsHolder.cs ================================================ namespace Validot.Factory { using System; using Validot.Settings; public interface ISettingsHolder { Func Settings { get; } } } ================================================ FILE: src/Validot/Factory/ISpecificationHolder.cs ================================================ namespace Validot { /// /// Holds specification for the models of type . /// /// Type of the specification that this instance holds. public interface ISpecificationHolder { /// /// Gets specification for the models of type . /// Specification Specification { get; } } } ================================================ FILE: src/Validot/Factory/ValidatorFactory.cs ================================================ namespace Validot.Factory { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Validot.Settings; using Validot.Validation.Scheme; /// /// Creates instances of . /// public sealed class ValidatorFactory { /// /// Creates instance of that can validate objects of type against the provided specification. /// /// Specification used to validate models. /// Settings builder that helps adjust the created 's settings. If not present, the default values are provided. /// Type of the models that this instance of can validate. /// Instance of , fully initialized and ready to work. public IValidator Create(Specification specification, Func settings = null) { var resolvedSettings = GetResolvedSettings(ValidatorSettings.GetDefault(), settings); var modelScheme = ModelSchemeFactory.Create(specification); SetReferenceLoopProtection(resolvedSettings, modelScheme.IsReferenceLoopPossible); return Create(specification, resolvedSettings); } /// /// Creates instance of that can validate objects of type against the specification provided in the . /// /// Object that provides specification used to validate models (its member ) and, optionally, settings (, if it implements also . /// Settings builder that helps adjust the created 's settings. If not present, the default values are provided. Overrides translations delivered by , if it implements also . /// Type of the models that this instance of can validate. /// Instance of , fully initialized and ready to work. /// Thrown if is null. /// Thrown if 's is null. public IValidator Create(ISpecificationHolder specificationHolder, Func settings = null) { if (specificationHolder is null) { throw new ArgumentNullException(nameof(specificationHolder)); } var validatorSettings = ValidatorSettings.GetDefault(); if (specificationHolder is ISettingsHolder validatorSettingsHolder) { if (validatorSettingsHolder.Settings is null) { throw new ArgumentException($"{nameof(ISettingsHolder)} can't have null {nameof(ISettingsHolder.Settings)}", nameof(specificationHolder)); } validatorSettings = GetResolvedSettings(validatorSettings, validatorSettingsHolder.Settings); } validatorSettings = GetResolvedSettings(validatorSettings, settings); var modelScheme = ModelSchemeFactory.Create(specificationHolder.Specification); SetReferenceLoopProtection(validatorSettings, modelScheme.IsReferenceLoopPossible); return Create(specificationHolder.Specification, validatorSettings); } /// /// Creates instance of that can validate objects of type against the provided specification. /// /// Specification used to validate models. /// Settings used to validate models. /// Type of the models that this instance of can validate. /// Instance of , fully initialized and ready to work. /// Thrown if is null. /// Thrown if is not an instance of . public IValidator Create(Specification specification, IValidatorSettings settings) { var modelScheme = ModelSchemeFactory.Create(specification); if (settings is null) { throw new ArgumentNullException(nameof(settings)); } if (!(settings is ValidatorSettings validatorSettings)) { throw new ArgumentException($"Custom {nameof(IValidatorSettings)} implementations are not supported.", nameof(settings)); } validatorSettings.IsLocked = true; return new Validator(modelScheme, settings); } /// /// Fetches information about the specification holders contained in the given assemblies and provides the way to create the validators out of them. Helps with populating dependency injection containers. /// /// Assemblies to scan for specification holders. Must not be empty. If you don't know what to do, try passing AppDomain.CurrentDomain.GetAssemblies(). /// Collection of items containing information about the specification holders found in provided assembly and a method to create the validators out of them. public IReadOnlyList FetchHolders(params Assembly[] assemblies) { ThrowHelper.NullArgument(assemblies, nameof(assemblies)); ThrowHelper.NullInCollection(assemblies, nameof(assemblies)); if (assemblies.Length == 0) { throw new ArgumentException("Assembly collection must not be empty", nameof(assemblies)); } var holders = new List(); var holderTypes = assemblies.SelectMany(GetAllSpecificationHoldersFromAssembly).ToList(); foreach (var holderType in holderTypes) { var specificationHolderTypes = holderType.GetInterfaces().Where(IsSpecificationHolderInterface).ToList(); foreach (var specificationHolderType in specificationHolderTypes) { var specifiedType = specificationHolderType.GetGenericArguments().Single(); holders.Add(new HolderInfo(holderType, specifiedType)); } } return holders; IReadOnlyList GetAllSpecificationHoldersFromAssembly(Assembly assembly) { return assembly .GetTypes() .Where(type => type.IsClass && type.GetConstructor(Type.EmptyTypes) != null && type.GetInterfaces().Any(IsSpecificationHolderInterface)) .ToArray(); } bool IsSpecificationHolderInterface(Type @interface) { return @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(ISpecificationHolder<>).GetGenericTypeDefinition(); } } private static void SetReferenceLoopProtection(ValidatorSettings settings, bool isReferenceLoopPossible) { if (!settings.ReferenceLoopProtectionEnabled.HasValue) { _ = isReferenceLoopPossible ? settings.WithReferenceLoopProtection() : settings.WithReferenceLoopProtectionDisabled(); } } private static ValidatorSettings GetResolvedSettings(ValidatorSettings initSettings, Func settingsBuilder) { var resolvedSettings = settingsBuilder is null ? initSettings : settingsBuilder(initSettings); if (!ReferenceEquals(initSettings, resolvedSettings)) { throw new InvalidOperationException("Validator settings fluent API should return the same reference as received."); } return resolvedSettings; } } } ================================================ FILE: src/Validot/IValidator.cs ================================================ namespace Validot { using Validot.Results; using Validot.Settings; /// /// Validator validates objects of type according to the specification. /// /// /// The type of models that this instance can validate. public interface IValidator { /// /// Gets settings of this instance. /// IValidatorSettings Settings { get; } /// /// Gets the validation result that contains all possible paths and errors described in the specification. /// It's the specification in a form of . /// For collection, the path contains only '#' instead of the item's index. /// For reference loop's root, the error is replaced with the single message under the key 'Global.ReferenceLoop'. /// IValidationResult Template { get; } /// /// Quickly verifies whether the model is valid (according to the specification) or not. /// This is highly-optimized version of , but it doesn't return any information about errors. /// /// The model to be validated. /// True, if model is valid and there are no errors according to the specification. Otherwise - false. bool IsValid(T model); /// /// Validates the model against the specification. Returns object that contains full information about the errors found during the validation process. /// WARNING! The returned object is internally coupled with the instance of that created it. /// It's safe to use its members to get the information you want and process them further, but don't cache the instance of itself or pass it around your system too much. /// /// The model to be validated. /// If true, the validation process will stop after detecting the first error. Otherwise, full validation is performed. /// Full information (in a form of about the errors found during the validation process, their location, messages and codes. IValidationResult Validate(T model, bool failFast = false); } } ================================================ FILE: src/Validot/PathHelper.cs ================================================ namespace Validot { using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; internal static class PathHelper { private const char UpperLevelPointerChar = '<'; private const char DividerChar = '.'; private const char CollectionIndexPrefixChar = '#'; private static readonly Regex CollectionItemSegmentRegex = new Regex(@"(?<=(^|\.))#\d{0,}(?=(\.|$))", RegexOptions.Compiled); private static readonly string _indexInTheMiddle = $"{Divider}{CollectionIndexPrefix}{Divider}"; private static readonly string _indexAtStart = $"{CollectionIndexPrefix}{Divider}"; private static readonly string _indexAtEnd = $"{Divider}{CollectionIndexPrefix}"; public static char UpperLevelPointer => UpperLevelPointerChar; public static char Divider => DividerChar; public static char CollectionIndexPrefix => CollectionIndexPrefixChar; public static string CollectionIndexPrefixString { get; } = char.ToString(CollectionIndexPrefix); public static string ResolvePath(string basePath, string relativePath) { ThrowHelper.NullArgument(basePath, nameof(basePath)); ThrowHelper.NullArgument(relativePath, nameof(relativePath)); if (relativePath.Length == 0) { return basePath; } if (basePath.Length == 0) { if (relativePath[0] != UpperLevelPointer) { return relativePath; } } if (relativePath[0] == UpperLevelPointer) { var up = 0; while (++up < relativePath.Length) { if (relativePath[up] != UpperLevelPointer) { break; } } var newSegmentCore = relativePath.TrimStart(UpperLevelPointer); for (var i = basePath.Length - 2; i >= 0; --i) { if (basePath[i] == Divider && --up == 0) { if (newSegmentCore.Length == 0) { return basePath.Substring(0, i); } return basePath.Substring(0, i + 1) + newSegmentCore; } } return newSegmentCore; } return $"{basePath}.{relativePath}"; } public static string GetWithoutIndexes(string path) { return CollectionItemSegmentRegex.Replace(path, CollectionIndexPrefixString); } public static string GetWithIndexes(string path, IReadOnlyCollection indexesStack) { if (indexesStack.Count == 0) { return path; } var isSingleIndex = indexesStack.Count == 1 && path.Length == 1 && path[0] == CollectionIndexPrefix; var builder = !isSingleIndex ? new StringBuilder(path) : null; var i = 0; foreach (var index in indexesStack) { if (isSingleIndex) { return FormatCollectionIndex(index); } if (i == 0 && builder.ToString().EndsWith(_indexAtEnd, StringComparison.Ordinal)) { _ = builder.Replace(_indexAtEnd, $"{Divider}{FormatCollectionIndex(index)}", builder.Length - 2, 2); continue; } var pointer = builder.ToString().LastIndexOf(_indexInTheMiddle, StringComparison.InvariantCulture); if (pointer != -1) { _ = builder.Replace(_indexInTheMiddle, $"{Divider}{FormatCollectionIndex(index)}{Divider}", pointer, 3); } else if (builder.ToString().StartsWith(_indexAtStart, StringComparison.Ordinal)) { _ = builder.Replace(_indexAtStart, $"{FormatCollectionIndex(index)}{Divider}", 0, 2); break; } ++i; } return builder.ToString(); } public static bool ContainsIndexes(string path) { return CollectionItemSegmentRegex.IsMatch(path); } public static int GetIndexesAmount(string path) { return CollectionItemSegmentRegex.Matches(path).Count; } public static string GetLastLevel(string path) { var lastDividerIndex = path.LastIndexOf(Divider); if (lastDividerIndex == -1) { return path; } return path.Substring(lastDividerIndex + 1); } public static bool IsValidAsPath(string path) { if (string.IsNullOrEmpty(path)) { return false; } path = path.TrimStart('<'); if (path.StartsWith(".", StringComparison.Ordinal) || path.EndsWith(".", StringComparison.Ordinal)) { return false; } if (path.IndexOf("..", StringComparison.Ordinal) != -1) { return false; } return true; } public static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) { return " "; } path = path.TrimStart('<').Trim('.'); while (path.IndexOf("..", StringComparison.Ordinal) != -1) { path = path.Replace("..", "."); } return path; } private static string FormatCollectionIndex(string index) { return $"{CollectionIndexPrefix}{index}"; } } } ================================================ FILE: src/Validot/Properties/AssemblyInfo.cs ================================================ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Validot.Tests.Unit")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DynamicProxyGenAssembly2")] [assembly: System.Reflection.AssemblyVersion("0.0.0.0")] // this line is autogenerated by the build script [assembly: System.Reflection.AssemblyFileVersion("0.0.0.0")] // this line is autogenerated by the build script [assembly: System.Reflection.AssemblyProduct("Validot")] [assembly: System.Reflection.AssemblyCopyright("Bartosz Lenar")] ================================================ FILE: src/Validot/Results/IValidationResult.cs ================================================ namespace Validot.Results { using System.Collections.Generic; /// /// Object that contains full information about the errors found during the validation process. /// WARNING! This object is internally coupled with the instance of that created it. /// It's safe to use its members to get the information you want and process them further, but don't cache the instance of itself or pass it around your system too much. /// public interface IValidationResult { /// /// Gets a value indicating whether errors were detected during the validation process. /// bool AnyErrors { get; } /// /// Gets all the paths with errors detected during the validation process. /// If the collection doesn't contain certain path, it means that no errors were detected for it. /// IReadOnlyCollection Paths { get; } /// /// Gets collection (without duplicates) of all the error codes saved during the validation process. /// IReadOnlyCollection Codes { get; } /// /// Gets code map - a dictionary that links error codes with their paths. /// The key is the path, and the value is the list of error codes saved for this path during the validation process. /// If the dictionary doesn't contain certain path, it means that no error codes were saved for this path. /// IReadOnlyDictionary> CodeMap { get; } /// /// Gets message map - a dictionary that links error messages with their paths. /// The key is the path, and the value is the list of error messages saved for this path during the validation process. /// If the dictionary doesn't contain certain path, it means that no error messages were saved for this path. /// IReadOnlyDictionary> MessageMap { get; } /// /// Gets list of translation names that can be used to translate error messages using method. /// Translation names are the same as in the that produces this instance of . /// IReadOnlyList TranslationNames { get; } /// /// Gets message map - a dictionary that links error messages with their paths. /// This is the same content as the one received in , but the error messages are translated using the given translation name. /// /// Name of the translation that will be used to translate all error messages in the map. For the available input values, check the property. /// Dictionary where the key is the path, and the value is the list of error messages saved for this path during the validation process. IReadOnlyDictionary> GetTranslatedMessageMap(string translationName); /// /// Returns friendly printing of all error codes (in the first line) and messages (each one in a separate line, preceded with its paths). /// First line contains error codes - comma separated values from collection. /// Codes are followed with error messages - each message in a separate line, preceded with its path. /// /// Name of the translation that will be used to translate all error messages in the printing. For the available input values, check the property. /// String containing two sections separated with an empty line: error codes (values from , in the first line, coma separated) and messages (each one in a separate line, preceded with its path). string ToString(string translationName); } } ================================================ FILE: src/Validot/Results/ValidationResult.cs ================================================ namespace Validot.Results { using System; using System.Collections.Generic; using System.Linq; using System.Text; using Validot.Errors; internal class ValidationResult : IValidationResult { private const string NoErrorsString = "OK"; private const string PathSeparator = ": "; private const string CodeSeparator = ", "; private static readonly IReadOnlyDictionary> EmptyDictionary = new Dictionary>(); private readonly IMessageService _messageService; private readonly Dictionary> _resultErrors; private readonly IReadOnlyDictionary _errorRegistry; private IReadOnlyCollection _codes; private IReadOnlyDictionary> _codeMap; private IReadOnlyDictionary> _messageMap; public ValidationResult(Dictionary> resultErrors, IReadOnlyDictionary errorRegistry, IMessageService messageService) { AnyErrors = resultErrors.Count != 0; _resultErrors = resultErrors; _errorRegistry = errorRegistry; _messageService = messageService; } public static ValidationResult NoErrorsResult { get; } = new ValidationResult(new Dictionary>(), new Dictionary(), null); public bool AnyErrors { get; } public IReadOnlyCollection Paths => _resultErrors.Keys; public IReadOnlyCollection Codes => _codes ?? (_codes = GetCodes()); public IReadOnlyDictionary> CodeMap => _codeMap ?? (_codeMap = GetCodeMap()); public IReadOnlyDictionary> MessageMap => _messageMap ?? (_messageMap = GetTranslatedMessageMap(null)); public IReadOnlyList TranslationNames => _messageService?.TranslationNames ?? Array.Empty(); public IReadOnlyDictionary> GetTranslatedMessageMap(string translationName) { return AnyErrors ? _messageService.GetMessages(_resultErrors, translationName) : EmptyDictionary; } public override string ToString() { return ToString(null); } public string ToString(string translationName) { if (!AnyErrors) { return NoErrorsString; } var messageMap = translationName is null ? MessageMap : GetTranslatedMessageMap(translationName); var (capacity, lines) = EstimateCapacityAndLines(messageMap, Codes); var stringBuilder = new StringBuilder(capacity); if (Codes.Count > 0) { var codeCounter = 0; foreach (var code in Codes) { _ = stringBuilder.Append(code); if (++codeCounter < Codes.Count) { _ = stringBuilder.Append(CodeSeparator); } } } if (messageMap.Count > 0) { var linesCount = 0; if (Codes.Count > 0) { _ = stringBuilder.Append(Environment.NewLine); _ = stringBuilder.Append(Environment.NewLine); linesCount = 3; } foreach (var pair in messageMap) { foreach (var message in pair.Value) { if (pair.Key.Length == 0) { _ = stringBuilder.Append($"{message}"); } else { _ = stringBuilder.Append($"{pair.Key}{PathSeparator}{message}"); } if (++linesCount < lines) { _ = stringBuilder.Append(Environment.NewLine); } } } } return stringBuilder.ToString(); } internal IReadOnlyDictionary> GetErrorOutput() { var result = new Dictionary>(_resultErrors.Count); foreach (var pair in _resultErrors) { var errors = new IError[pair.Value.Count]; for (var i = 0; i < pair.Value.Count; ++i) { errors[i] = _errorRegistry[pair.Value[i]]; } result.Add(pair.Key, errors); } return result; } private static (int capacity, int lines) EstimateCapacityAndLines(IReadOnlyDictionary> mMap, IReadOnlyCollection cMap) { var lines = 0; var capacity = 10; foreach (var pair in mMap) { foreach (var message in pair.Value) { if (pair.Key.Length > 0) { capacity += pair.Key.Length + PathSeparator.Length; } capacity += message.Length; } lines += pair.Value.Count; capacity += (pair.Value.Count - 1) * Environment.NewLine.Length; } if (cMap.Count > 0) { foreach (var code in cMap) { capacity += code.Length; } capacity += (cMap.Count - 1) * CodeSeparator.Length; lines += 1; } if (mMap.Count > 0 && cMap.Count > 0) { capacity += 2 * Environment.NewLine.Length; lines += 2; } return (capacity, lines); } private IReadOnlyCollection GetCodes() { if (!AnyErrors) { return Array.Empty(); } var result = new HashSet(); foreach (var pair in _resultErrors) { for (var i = 0; i < pair.Value.Count; ++i) { if (_errorRegistry[pair.Value[i]].Codes?.Any() == true) { for (var j = 0; j < _errorRegistry[pair.Value[i]].Codes.Count; ++j) { if (!result.Contains(_errorRegistry[pair.Value[i]].Codes[j])) { _ = result.Add(_errorRegistry[pair.Value[i]].Codes[j]); } } } } } return result; } private IReadOnlyDictionary> GetCodeMap() { if (!AnyErrors) { return EmptyDictionary; } var pathCapacity = 0; foreach (var pair in _resultErrors) { for (var i = 0; i < pair.Value.Count; ++i) { if (_errorRegistry[pair.Value[i]].Codes?.Any() == true) { ++pathCapacity; break; } } } if (pathCapacity == 0) { return EmptyDictionary; } var dictionary = new Dictionary>(pathCapacity); foreach (var pair in _resultErrors) { var codesCapacity = 0; for (var i = 0; i < pair.Value.Count; ++i) { codesCapacity += _errorRegistry[pair.Value[i]].Codes?.Count ?? 0; } if (codesCapacity == 0) { continue; } var codes = new string[codesCapacity]; var codesPointer = 0; for (var i = 0; i < pair.Value.Count; ++i) { if (_errorRegistry[pair.Value[i]].Codes is null || _errorRegistry[pair.Value[i]].Codes.Count == 0) { continue; } for (var j = 0; j < _errorRegistry[pair.Value[i]].Codes.Count; ++j) { codes[codesPointer++] = _errorRegistry[pair.Value[i]].Codes[j]; } } dictionary.Add(pair.Key, codes); } return dictionary; } } } ================================================ FILE: src/Validot/Rules/BoolRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class BoolRules { public static IRuleOut True(this IRuleIn @this) { return @this.RuleTemplate(v => v, MessageKey.BoolType.True); } public static IRuleOut True(this IRuleIn @this) { return @this.RuleTemplate(v => v.Value, MessageKey.BoolType.True); } public static IRuleOut False(this IRuleIn @this) { return @this.RuleTemplate(v => !v, MessageKey.BoolType.False); } public static IRuleOut False(this IRuleIn @this) { return @this.RuleTemplate(v => !v.Value, MessageKey.BoolType.False); } } } ================================================ FILE: src/Validot/Rules/CharRules.cs ================================================ namespace Validot { using System; using System.Globalization; using Validot.Specification; using Validot.Translations; public static class CharRules { public static IRuleOut EqualToIgnoreCase(this IRuleIn @this, char value) { return @this.RuleTemplate(v => string.Compare(v.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), value.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), StringComparison.Ordinal) == 0, MessageKey.CharType.EqualToIgnoreCase, Arg.Text(nameof(value), value)); } public static IRuleOut EqualToIgnoreCase(this IRuleIn @this, char value) { return @this.RuleTemplate(v => string.Compare(v.Value.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), value.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), StringComparison.Ordinal) == 0, MessageKey.CharType.EqualToIgnoreCase, Arg.Text(nameof(value), value)); } public static IRuleOut NotEqualToIgnoreCase(this IRuleIn @this, char value) { return @this.RuleTemplate(v => string.Compare(v.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), value.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), StringComparison.Ordinal) != 0, MessageKey.CharType.NotEqualToIgnoreCase, Arg.Text(nameof(value), value)); } public static IRuleOut NotEqualToIgnoreCase(this IRuleIn @this, char value) { return @this.RuleTemplate(v => string.Compare(v.Value.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), value.ToString(CultureInfo.InvariantCulture).ToUpperInvariant(), StringComparison.Ordinal) != 0, MessageKey.CharType.NotEqualToIgnoreCase, Arg.Text(nameof(value), value)); } } } ================================================ FILE: src/Validot/Rules/Collections/ArrayRules.cs ================================================ namespace Validot { using Validot.Specification; public static class ArrayRules { public static IRuleOut EmptyCollection(this IRuleIn @this) { return @this.EmptyCollection(); } public static IRuleOut NotEmptyCollection(this IRuleIn @this) { return @this.NotEmptyCollection(); } public static IRuleOut ExactCollectionSize(this IRuleIn @this, int size) { return @this.ExactCollectionSize(size); } public static IRuleOut MinCollectionSize(this IRuleIn @this, int min) { return @this.MinCollectionSize(min); } public static IRuleOut MaxCollectionSize(this IRuleIn @this, int max) { return @this.MaxCollectionSize(max); } public static IRuleOut CollectionSizeBetween(this IRuleIn @this, int min, int max) { return @this.CollectionSizeBetween(min, max); } } } ================================================ FILE: src/Validot/Rules/Collections/BaseCollectionRules.cs ================================================ namespace Validot { using System.Collections.Generic; using System.Linq; using Validot.Specification; using Validot.Translations; public static class BaseCollectionRules { public static IRuleOut EmptyCollection(this IRuleIn @this) where TCollection : IEnumerable { return @this.RuleTemplate(m => !m.Any(), MessageKey.Collections.EmptyCollection); } public static IRuleOut NotEmptyCollection(this IRuleIn @this) where TCollection : IEnumerable { return @this.RuleTemplate(m => m.Any(), MessageKey.Collections.NotEmptyCollection); } public static IRuleOut ExactCollectionSize(this IRuleIn @this, int size) where TCollection : IEnumerable { ThrowHelper.BelowZero(size, nameof(size)); return @this.RuleTemplate(m => m.Count() == size, MessageKey.Collections.ExactCollectionSize, Arg.Number(nameof(size), size)); } public static IRuleOut MinCollectionSize(this IRuleIn @this, int min) where TCollection : IEnumerable { ThrowHelper.BelowZero(min, nameof(min)); return @this.RuleTemplate(m => m.Count() >= min, MessageKey.Collections.MinCollectionSize, Arg.Number(nameof(min), min)); } public static IRuleOut MaxCollectionSize(this IRuleIn @this, int max) where TCollection : IEnumerable { ThrowHelper.BelowZero(max, nameof(max)); return @this.RuleTemplate(m => m.Count() <= max, MessageKey.Collections.MaxCollectionSize, Arg.Number(nameof(max), max)); } public static IRuleOut CollectionSizeBetween(this IRuleIn @this, int min, int max) where TCollection : IEnumerable { ThrowHelper.BelowZero(min, nameof(min)); ThrowHelper.BelowZero(max, nameof(max)); ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate( m => { var count = m.Count(); return count >= min && count <= max; }, MessageKey.Collections.CollectionSizeBetween, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } } } ================================================ FILE: src/Validot/Rules/Collections/IEnumerableRules.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; public static class IEnumerableRules { public static IRuleOut> EmptyCollection(this IRuleIn> @this) { return @this.EmptyCollection, TItem>(); } public static IRuleOut> NotEmptyCollection(this IRuleIn> @this) { return @this.NotEmptyCollection, TItem>(); } public static IRuleOut> ExactCollectionSize(this IRuleIn> @this, int size) { return @this.ExactCollectionSize, TItem>(size); } public static IRuleOut> MinCollectionSize(this IRuleIn> @this, int min) { return @this.MinCollectionSize, TItem>(min); } public static IRuleOut> MaxCollectionSize(this IRuleIn> @this, int max) { return @this.MaxCollectionSize, TItem>(max); } public static IRuleOut> CollectionSizeBetween(this IRuleIn> @this, int min, int max) { return @this.CollectionSizeBetween, TItem>(min, max); } } } ================================================ FILE: src/Validot/Rules/Collections/IListRules.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; public static class IListRules { public static IRuleOut> EmptyCollection(this IRuleIn> @this) { return @this.EmptyCollection, TItem>(); } public static IRuleOut> NotEmptyCollection(this IRuleIn> @this) { return @this.NotEmptyCollection, TItem>(); } public static IRuleOut> ExactCollectionSize(this IRuleIn> @this, int size) { return @this.ExactCollectionSize, TItem>(size); } public static IRuleOut> MinCollectionSize(this IRuleIn> @this, int min) { return @this.MinCollectionSize, TItem>(min); } public static IRuleOut> MaxCollectionSize(this IRuleIn> @this, int max) { return @this.MaxCollectionSize, TItem>(max); } public static IRuleOut> CollectionSizeBetween(this IRuleIn> @this, int min, int max) { return @this.CollectionSizeBetween, TItem>(min, max); } } } ================================================ FILE: src/Validot/Rules/Collections/IReadOnlyCollectionRules.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; public static class IReadOnlyCollectionRules { public static IRuleOut> EmptyCollection(this IRuleIn> @this) { return @this.EmptyCollection, TItem>(); } public static IRuleOut> NotEmptyCollection(this IRuleIn> @this) { return @this.NotEmptyCollection, TItem>(); } public static IRuleOut> ExactCollectionSize(this IRuleIn> @this, int size) { return @this.ExactCollectionSize, TItem>(size); } public static IRuleOut> MinCollectionSize(this IRuleIn> @this, int min) { return @this.MinCollectionSize, TItem>(min); } public static IRuleOut> MaxCollectionSize(this IRuleIn> @this, int max) { return @this.MaxCollectionSize, TItem>(max); } public static IRuleOut> CollectionSizeBetween(this IRuleIn> @this, int min, int max) { return @this.CollectionSizeBetween, TItem>(min, max); } } } ================================================ FILE: src/Validot/Rules/Collections/IReadOnlyListRules.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; public static class IReadOnlyListRules { public static IRuleOut> EmptyCollection(this IRuleIn> @this) { return @this.EmptyCollection, TItem>(); } public static IRuleOut> NotEmptyCollection(this IRuleIn> @this) { return @this.NotEmptyCollection, TItem>(); } public static IRuleOut> ExactCollectionSize(this IRuleIn> @this, int size) { return @this.ExactCollectionSize, TItem>(size); } public static IRuleOut> MinCollectionSize(this IRuleIn> @this, int min) { return @this.MinCollectionSize, TItem>(min); } public static IRuleOut> MaxCollectionSize(this IRuleIn> @this, int max) { return @this.MaxCollectionSize, TItem>(max); } public static IRuleOut> CollectionSizeBetween(this IRuleIn> @this, int min, int max) { return @this.CollectionSizeBetween, TItem>(min, max); } } } ================================================ FILE: src/Validot/Rules/Collections/ListRules.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; public static class ListRules { public static IRuleOut> EmptyCollection(this IRuleIn> @this) { return @this.EmptyCollection, TItem>(); } public static IRuleOut> NotEmptyCollection(this IRuleIn> @this) { return @this.NotEmptyCollection, TItem>(); } public static IRuleOut> ExactCollectionSize(this IRuleIn> @this, int size) { return @this.ExactCollectionSize, TItem>(size); } public static IRuleOut> MinCollectionSize(this IRuleIn> @this, int min) { return @this.MinCollectionSize, TItem>(min); } public static IRuleOut> MaxCollectionSize(this IRuleIn> @this, int max) { return @this.MaxCollectionSize, TItem>(max); } public static IRuleOut> CollectionSizeBetween(this IRuleIn> @this, int min, int max) { return @this.CollectionSizeBetween, TItem>(min, max); } } } ================================================ FILE: src/Validot/Rules/GuidRules.cs ================================================ namespace Validot.Rules { using System; using Validot.Specification; using Validot.Translations; public static class GuidRules { public static IRuleOut EqualTo(this IRuleIn @this, Guid value) { return @this.RuleTemplate(v => v == value, MessageKey.GuidType.EqualTo, Arg.GuidValue(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, Guid value) { return @this.RuleTemplate(v => v.Value == value, MessageKey.GuidType.EqualTo, Arg.GuidValue(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, Guid value) { return @this.RuleTemplate(v => v != value, MessageKey.GuidType.NotEqualTo, Arg.GuidValue(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, Guid value) { return @this.RuleTemplate(v => v.Value != value, MessageKey.GuidType.NotEqualTo, Arg.GuidValue(nameof(value), value)); } public static IRuleOut NotEmpty(this IRuleIn @this) { return @this.RuleTemplate(v => v != Guid.Empty, MessageKey.GuidType.NotEmpty); } public static IRuleOut NotEmpty(this IRuleIn @this) { return @this.RuleTemplate(v => v.Value != Guid.Empty, MessageKey.GuidType.NotEmpty); } } } ================================================ FILE: src/Validot/Rules/Numbers/ByteRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class ByteRules { public static IRuleOut EqualTo(this IRuleIn @this, byte value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, byte value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, byte value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, byte value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, byte min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, byte min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, byte min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, byte min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, byte max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, byte max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, byte max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, byte max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, byte min, byte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, byte min, byte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, byte min, byte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, byte min, byte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: src/Validot/Rules/Numbers/CharNumbersRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class CharNumbersRules { public static IRuleOut EqualTo(this IRuleIn @this, char value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, char value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, char value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, char value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, char min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, char min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, char min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, char min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, char max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, char max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, char max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, char max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, char min, char max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, char min, char max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, char min, char max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, char min, char max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: src/Validot/Rules/Numbers/DecimalRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class DecimalRules { public static IRuleOut EqualTo(this IRuleIn @this, decimal value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, decimal value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, decimal value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, decimal value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, decimal min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, decimal min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, decimal min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, decimal min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, decimal max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, decimal max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, decimal max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, decimal max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, decimal min, decimal max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, decimal min, decimal max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, decimal min, decimal max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, decimal min, decimal max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: src/Validot/Rules/Numbers/DoubleRules.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Translations; public static class DoubleRules { public static IRuleOut EqualTo(this IRuleIn @this, double value, double tolerance = 0.0000001d) { return @this.RuleTemplate(m => AreEqual(m, value, tolerance), MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut EqualTo(this IRuleIn @this, double value, double tolerance = 0.0000001d) { return @this.RuleTemplate(m => AreEqual(m.Value, value, tolerance), MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NotEqualTo(this IRuleIn @this, double value, double tolerance = 0.0000001d) { return @this.RuleTemplate(m => !AreEqual(m, value, tolerance), MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NotEqualTo(this IRuleIn @this, double value, double tolerance = 0.0000001d) { return @this.RuleTemplate(m => !AreEqual(m.Value, value, tolerance), MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut GreaterThan(this IRuleIn @this, double min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, double min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, double max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, double max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, double min, double max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, double min, double max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this, double tolerance = 0.0000001d) { return @this.RuleTemplate(m => !AreEqual(m, 0d, tolerance), MessageKey.Numbers.NonZero, Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NonZero(this IRuleIn @this, double tolerance = 0.0000001d) { return @this.RuleTemplate(m => !AreEqual(m.Value, 0d, tolerance), MessageKey.Numbers.NonZero, Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NonNaN(this IRuleIn @this) { return @this.RuleTemplate(m => !double.IsNaN(m), MessageKey.Numbers.NonNaN); } public static IRuleOut NonNaN(this IRuleIn @this) { return @this.RuleTemplate(m => !double.IsNaN(m.Value), MessageKey.Numbers.NonNaN); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } private static bool AreEqual(double a, double b, double tolerance) { return Math.Abs(a - b) < tolerance; } } } ================================================ FILE: src/Validot/Rules/Numbers/FloatRules.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Translations; public static class FloatRules { public static IRuleOut EqualTo(this IRuleIn @this, float value, float tolerance = 0.0000001f) { return @this.RuleTemplate(m => AreEqual(m, value, tolerance), MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut EqualTo(this IRuleIn @this, float value, float tolerance = 0.0000001f) { return @this.RuleTemplate(m => AreEqual(m.Value, value, tolerance), MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NotEqualTo(this IRuleIn @this, float value, float tolerance = 0.0000001f) { return @this.RuleTemplate(m => !AreEqual(m, value, tolerance), MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NotEqualTo(this IRuleIn @this, float value, float tolerance = 0.0000001f) { return @this.RuleTemplate(m => !AreEqual(m.Value, value, tolerance), MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value), Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut GreaterThan(this IRuleIn @this, float min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, float min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, float max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, float max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, float min, float max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, float min, float max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this, float tolerance = 0.0000001f) { return @this.RuleTemplate(m => !AreEqual(m, 0f, tolerance), MessageKey.Numbers.NonZero, Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NonZero(this IRuleIn @this, float tolerance = 0.0000001f) { return @this.RuleTemplate(m => !AreEqual(m.Value, 0f, tolerance), MessageKey.Numbers.NonZero, Arg.Number(nameof(tolerance), tolerance)); } public static IRuleOut NonNaN(this IRuleIn @this) { return @this.RuleTemplate(m => !float.IsNaN(m), MessageKey.Numbers.NonNaN); } public static IRuleOut NonNaN(this IRuleIn @this) { return @this.RuleTemplate(m => !float.IsNaN(m.Value), MessageKey.Numbers.NonNaN); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } private static bool AreEqual(float a, float b, float tolerance) { return Math.Abs(a - b) < tolerance; } } } ================================================ FILE: src/Validot/Rules/Numbers/IntRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class IntRules { public static IRuleOut EqualTo(this IRuleIn @this, int value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, int value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, int value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, int value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, int min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, int min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, int min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, int min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, int max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, int max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, int max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, int max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, int min, int max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, int min, int max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, int min, int max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, int min, int max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: src/Validot/Rules/Numbers/LongRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class LongRules { public static IRuleOut EqualTo(this IRuleIn @this, long value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, long value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, long value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, long value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, long min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, long min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, long min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, long min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, long max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, long max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, long max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, long max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, long min, long max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, long min, long max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, long min, long max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, long min, long max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: src/Validot/Rules/Numbers/SByteRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class SByteRules { public static IRuleOut EqualTo(this IRuleIn @this, sbyte value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, sbyte value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, sbyte value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, sbyte value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, sbyte min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, sbyte min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, sbyte min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, sbyte min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, sbyte max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, sbyte max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, sbyte max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, sbyte max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, sbyte min, sbyte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, sbyte min, sbyte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, sbyte min, sbyte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, sbyte min, sbyte max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: src/Validot/Rules/Numbers/ShortRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class ShortRules { public static IRuleOut EqualTo(this IRuleIn @this, short value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, short value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, short value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, short value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, short min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, short min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, short min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, short min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, short max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, short max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, short max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, short max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, short min, short max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, short min, short max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, short min, short max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, short min, short max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m < 0, MessageKey.Numbers.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value < 0, MessageKey.Numbers.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m >= 0, MessageKey.Numbers.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value >= 0, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: src/Validot/Rules/Numbers/UIntRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class UIntRules { public static IRuleOut EqualTo(this IRuleIn @this, uint value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, uint value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, uint value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, uint value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, uint min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, uint min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, uint min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, uint min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, uint max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, uint max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, uint max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, uint max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, uint min, uint max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, uint min, uint max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, uint min, uint max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, uint min, uint max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: src/Validot/Rules/Numbers/ULongRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class ULongRules { public static IRuleOut EqualTo(this IRuleIn @this, ulong value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, ulong value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, ulong value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, ulong value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, ulong min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, ulong min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, ulong min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, ulong min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, ulong max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, ulong max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, ulong max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, ulong max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, ulong min, ulong max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, ulong min, ulong max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, ulong min, ulong max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, ulong min, ulong max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: src/Validot/Rules/Numbers/UShortRules.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Translations; public static class UShortRules { public static IRuleOut EqualTo(this IRuleIn @this, ushort value) { return @this.RuleTemplate(m => m == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, ushort value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.Numbers.EqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, ushort value) { return @this.RuleTemplate(m => m != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, ushort value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.Numbers.NotEqualTo, Arg.Number(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, ushort min) { return @this.RuleTemplate(m => m > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, ushort min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.Numbers.GreaterThan, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, ushort min) { return @this.RuleTemplate(m => m >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, ushort min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, ushort max) { return @this.RuleTemplate(m => m < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, ushort max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.Numbers.LessThan, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, ushort max) { return @this.RuleTemplate(m => m <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, ushort max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, ushort min, ushort max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, ushort min, ushort max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.Numbers.Between, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, ushort min, ushort max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, ushort min, ushort max) { ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m != 0, MessageKey.Numbers.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value != 0, MessageKey.Numbers.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m > 0, MessageKey.Numbers.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value > 0, MessageKey.Numbers.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m <= 0, MessageKey.Numbers.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value <= 0, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: src/Validot/Rules/Text/EmailRules.cs ================================================ namespace Validot { using System; using System.Globalization; using System.Text.RegularExpressions; using Validot.Specification; using Validot.Translations; public static class EmailRules { private static readonly Regex EmailDomainRegex = new Regex(@"(@)(.+)$", RegexOptions.Compiled, TimeSpan.FromMilliseconds(200)); private static readonly Regex EmailRegex = new Regex(@"^(?("")("".+?(? Email(this IRuleIn @this, EmailValidationMode mode = EmailValidationMode.ComplexRegex) { if (!Enum.IsDefined(typeof(EmailValidationMode), mode)) { throw new ArgumentException("Invalid EmailValidationMode value", nameof(mode)); } if (mode == EmailValidationMode.DataAnnotationsCompatible) { return @this.RuleTemplate(IsEmailValidAccordingToDataAnnotations, MessageKey.Texts.Email); } return @this.RuleTemplate(IsEmailValidAccordingToRegex, MessageKey.Texts.Email); } private static bool IsEmailValidAccordingToRegex(string email) { // Entirely copy-pasted from https://docs.microsoft.com/en-us/dotnet/standard/base-types/how-to-verify-that-strings-are-in-valid-email-format if (string.IsNullOrWhiteSpace(email)) { return false; } try { email = EmailDomainRegex.Replace(email, DomainMapper); string DomainMapper(Match match) { var idn = new IdnMapping(); var domainName = idn.GetAscii(match.Groups[2].Value); return match.Groups[1].Value + domainName; } } catch (RegexMatchTimeoutException) { return false; } catch (ArgumentException) { return false; } try { return EmailRegex.IsMatch(email); } catch (RegexMatchTimeoutException) { return false; } } private static bool IsEmailValidAccordingToDataAnnotations(string email) { // Method copy pasted from EmailAddressAttribute.cs: // https://github.com/dotnet/runtime/blob/master/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/EmailAddressAttribute.cs // only return true if there is only 1 '@' character // and it is neither the first nor the last character int index = email.IndexOf('@'); return index > 0 && index != email.Length - 1 && index == email.LastIndexOf('@'); } } } ================================================ FILE: src/Validot/Rules/Text/EmailValidationMode.cs ================================================ namespace Validot { public enum EmailValidationMode { ComplexRegex = 0, DataAnnotationsCompatible = 1, } } ================================================ FILE: src/Validot/Rules/Text/StringRules.cs ================================================ namespace Validot { using System; using System.Text.RegularExpressions; using Validot.Specification; using Validot.Translations; public static class StringRules { public static IRuleOut EqualTo(this IRuleIn @this, string value, StringComparison stringComparison = StringComparison.Ordinal) { ThrowHelper.NullArgument(value, nameof(value)); return @this.RuleTemplate(v => string.Equals(v, value, stringComparison), MessageKey.Texts.EqualTo, Arg.Text(nameof(value), value), Arg.Enum(nameof(stringComparison), stringComparison)); } public static IRuleOut NotEqualTo(this IRuleIn @this, string value, StringComparison stringComparison = StringComparison.Ordinal) { ThrowHelper.NullArgument(value, nameof(value)); return @this.RuleTemplate(v => !string.Equals(v, value, stringComparison), MessageKey.Texts.NotEqualTo, Arg.Text(nameof(value), value), Arg.Enum(nameof(stringComparison), stringComparison)); } public static IRuleOut Contains(this IRuleIn @this, string value, StringComparison stringComparison = StringComparison.Ordinal) { ThrowHelper.NullArgument(value, nameof(value)); return @this.RuleTemplate(v => v.IndexOf(value, stringComparison) >= 0, MessageKey.Texts.Contains, Arg.Text(nameof(value), value), Arg.Enum(nameof(stringComparison), stringComparison)); } public static IRuleOut NotContains(this IRuleIn @this, string value, StringComparison stringComparison = StringComparison.Ordinal) { ThrowHelper.NullArgument(value, nameof(value)); return @this.RuleTemplate(v => v.IndexOf(value, stringComparison) < 0, MessageKey.Texts.NotContains, Arg.Text(nameof(value), value), Arg.Enum(nameof(stringComparison), stringComparison)); } public static IRuleOut NotEmpty(this IRuleIn @this) { return @this.RuleTemplate(v => !string.IsNullOrEmpty(v), MessageKey.Texts.NotEmpty); } public static IRuleOut NotWhiteSpace(this IRuleIn @this) { return @this.RuleTemplate(v => !string.IsNullOrWhiteSpace(v), MessageKey.Texts.NotWhiteSpace); } public static IRuleOut SingleLine(this IRuleIn @this) { return @this.RuleTemplate(v => !v.Contains(Environment.NewLine), MessageKey.Texts.SingleLine); } public static IRuleOut ExactLength(this IRuleIn @this, int length) { ThrowHelper.BelowZero(length, nameof(length)); return @this.RuleTemplate(v => v.Replace(Environment.NewLine, " ").Length == length, MessageKey.Texts.ExactLength, Arg.Number(nameof(length), length)); } public static IRuleOut MaxLength(this IRuleIn @this, int max) { ThrowHelper.BelowZero(max, nameof(max)); return @this.RuleTemplate(v => v.Replace(Environment.NewLine, " ").Length <= max, MessageKey.Texts.MaxLength, Arg.Number(nameof(max), max)); } public static IRuleOut MinLength(this IRuleIn @this, int min) { ThrowHelper.BelowZero(min, nameof(min)); return @this.RuleTemplate(v => v.Replace(Environment.NewLine, " ").Length >= min, MessageKey.Texts.MinLength, Arg.Number(nameof(min), min)); } public static IRuleOut LengthBetween(this IRuleIn @this, int min, int max) { ThrowHelper.BelowZero(min, nameof(min)); ThrowHelper.BelowZero(max, nameof(max)); ThrowHelper.InvalidRange(min, nameof(min), max, nameof(max)); return @this.RuleTemplate( v => { var squashedLength = v.Replace(Environment.NewLine, " ").Length; return squashedLength >= min && squashedLength <= max; }, MessageKey.Texts.LengthBetween, Arg.Number(nameof(min), min), Arg.Number(nameof(max), max)); } public static IRuleOut Matches(this IRuleIn @this, string pattern) { ThrowHelper.NullArgument(pattern, nameof(pattern)); return @this.RuleTemplate(v => Regex.IsMatch(v, pattern, RegexOptions.CultureInvariant), MessageKey.Texts.Matches, Arg.Text(nameof(pattern), pattern)); } public static IRuleOut Matches(this IRuleIn @this, Regex pattern) { ThrowHelper.NullArgument(pattern, nameof(pattern)); return @this.RuleTemplate(pattern.IsMatch, MessageKey.Texts.Matches, Arg.Text(nameof(pattern), pattern.ToString())); } public static IRuleOut StartsWith(this IRuleIn @this, string value, StringComparison stringComparison = StringComparison.Ordinal) { ThrowHelper.NullArgument(value, nameof(value)); return @this.RuleTemplate(v => v.StartsWith(value, stringComparison), MessageKey.Texts.StartsWith, Arg.Text(nameof(value), value), Arg.Enum(nameof(stringComparison), stringComparison)); } public static IRuleOut EndsWith(this IRuleIn @this, string value, StringComparison stringComparison = StringComparison.Ordinal) { ThrowHelper.NullArgument(value, nameof(value)); return @this.RuleTemplate(v => v.EndsWith(value, stringComparison), MessageKey.Texts.EndsWith, Arg.Text(nameof(value), value), Arg.Enum(nameof(stringComparison), stringComparison)); } } } ================================================ FILE: src/Validot/Rules/TimeSpanRules.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Translations; public static class TimeSpanRules { public static IRuleOut EqualTo(this IRuleIn @this, TimeSpan value) { return @this.RuleTemplate(m => m == value, MessageKey.TimeSpanType.EqualTo, Arg.Time(nameof(value), value)); } public static IRuleOut EqualTo(this IRuleIn @this, TimeSpan value) { return @this.RuleTemplate(m => m.Value == value, MessageKey.TimeSpanType.EqualTo, Arg.Time(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, TimeSpan value) { return @this.RuleTemplate(m => m != value, MessageKey.TimeSpanType.NotEqualTo, Arg.Time(nameof(value), value)); } public static IRuleOut NotEqualTo(this IRuleIn @this, TimeSpan value) { return @this.RuleTemplate(m => m.Value != value, MessageKey.TimeSpanType.NotEqualTo, Arg.Time(nameof(value), value)); } public static IRuleOut GreaterThan(this IRuleIn @this, TimeSpan min) { return @this.RuleTemplate(m => m > min, MessageKey.TimeSpanType.GreaterThan, Arg.Time(nameof(min), min)); } public static IRuleOut GreaterThan(this IRuleIn @this, TimeSpan min) { return @this.RuleTemplate(m => m.Value > min, MessageKey.TimeSpanType.GreaterThan, Arg.Time(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, TimeSpan min) { return @this.RuleTemplate(m => m >= min, MessageKey.TimeSpanType.GreaterThanOrEqualTo, Arg.Time(nameof(min), min)); } public static IRuleOut GreaterThanOrEqualTo(this IRuleIn @this, TimeSpan min) { return @this.RuleTemplate(m => m.Value >= min, MessageKey.TimeSpanType.GreaterThanOrEqualTo, Arg.Time(nameof(min), min)); } public static IRuleOut LessThan(this IRuleIn @this, TimeSpan max) { return @this.RuleTemplate(m => m < max, MessageKey.TimeSpanType.LessThan, Arg.Time(nameof(max), max)); } public static IRuleOut LessThan(this IRuleIn @this, TimeSpan max) { return @this.RuleTemplate(m => m.Value < max, MessageKey.TimeSpanType.LessThan, Arg.Time(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, TimeSpan max) { return @this.RuleTemplate(m => m <= max, MessageKey.TimeSpanType.LessThanOrEqualTo, Arg.Time(nameof(max), max)); } public static IRuleOut LessThanOrEqualTo(this IRuleIn @this, TimeSpan max) { return @this.RuleTemplate(m => m.Value <= max, MessageKey.TimeSpanType.LessThanOrEqualTo, Arg.Time(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, TimeSpan min, TimeSpan max) { ThrowHelper.InvalidRange(min.Ticks, nameof(min), max.Ticks, nameof(max)); return @this.RuleTemplate(m => m > min && m < max, MessageKey.TimeSpanType.Between, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max)); } public static IRuleOut Between(this IRuleIn @this, TimeSpan min, TimeSpan max) { ThrowHelper.InvalidRange(min.Ticks, nameof(min), max.Ticks, nameof(max)); return @this.RuleTemplate(m => m.Value > min && m.Value < max, MessageKey.TimeSpanType.Between, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, TimeSpan min, TimeSpan max) { ThrowHelper.InvalidRange(min.Ticks, nameof(min), max.Ticks, nameof(max)); return @this.RuleTemplate(m => m >= min && m <= max, MessageKey.TimeSpanType.BetweenOrEqualTo, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, TimeSpan min, TimeSpan max) { ThrowHelper.InvalidRange(min.Ticks, nameof(min), max.Ticks, nameof(max)); return @this.RuleTemplate(m => m.Value >= min && m.Value <= max, MessageKey.TimeSpanType.BetweenOrEqualTo, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max)); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Ticks != 0, MessageKey.TimeSpanType.NonZero); } public static IRuleOut NonZero(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value.Ticks != 0, MessageKey.TimeSpanType.NonZero); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Ticks > 0, MessageKey.TimeSpanType.Positive); } public static IRuleOut Positive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value.Ticks > 0, MessageKey.TimeSpanType.Positive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Ticks <= 0, MessageKey.TimeSpanType.NonPositive); } public static IRuleOut NonPositive(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value.Ticks <= 0, MessageKey.TimeSpanType.NonPositive); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Ticks < 0, MessageKey.TimeSpanType.Negative); } public static IRuleOut Negative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value.Ticks < 0, MessageKey.TimeSpanType.Negative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Ticks >= 0, MessageKey.TimeSpanType.NonNegative); } public static IRuleOut NonNegative(this IRuleIn @this) { return @this.RuleTemplate(m => m.Value.Ticks >= 0, MessageKey.TimeSpanType.NonNegative); } } } ================================================ FILE: src/Validot/Rules/Times/DateTimeFormats.cs ================================================ namespace Validot { public static class DateTimeFormats { public static string DateFormat { get; } = "yyyy-MM-dd"; public static string TimeFormat { get; } = "HH:mm:ss.FFFFFFF"; public static string DateAndTimeFormat { get; } = $"{DateFormat} {TimeFormat}"; public static string TimeSpanFormat { get; } = "c"; } } ================================================ FILE: src/Validot/Rules/Times/DateTimeOffsetRules.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Translations; public static class DateTimeOffsetRules { public static IRuleOut EqualTo(this IRuleIn @this, DateTimeOffset value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, value, timeComparison) == 0, MessageKey.Times.EqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut EqualTo(this IRuleIn @this, DateTimeOffset value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, value, timeComparison) == 0, MessageKey.Times.EqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut NotEqualTo(this IRuleIn @this, DateTimeOffset value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, value, timeComparison) != 0, MessageKey.Times.NotEqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut NotEqualTo(this IRuleIn @this, DateTimeOffset value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, value, timeComparison) != 0, MessageKey.Times.NotEqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut After(this IRuleIn @this, DateTimeOffset min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) > 0, MessageKey.Times.After, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut After(this IRuleIn @this, DateTimeOffset min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) > 0, MessageKey.Times.After, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut AfterOrEqualTo(this IRuleIn @this, DateTimeOffset min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) >= 0, MessageKey.Times.AfterOrEqualTo, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut AfterOrEqualTo(this IRuleIn @this, DateTimeOffset min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) >= 0, MessageKey.Times.AfterOrEqualTo, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Before(this IRuleIn @this, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, max, timeComparison) < 0, MessageKey.Times.Before, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Before(this IRuleIn @this, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, max, timeComparison) < 0, MessageKey.Times.Before, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BeforeOrEqualTo(this IRuleIn @this, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, max, timeComparison) <= 0, MessageKey.Times.BeforeOrEqualTo, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BeforeOrEqualTo(this IRuleIn @this, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, max, timeComparison) <= 0, MessageKey.Times.BeforeOrEqualTo, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Between(this IRuleIn @this, DateTimeOffset min, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) > 0 && TimeComparer.Compare(m, max, timeComparison) < 0, MessageKey.Times.Between, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Between(this IRuleIn @this, DateTimeOffset min, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) > 0 && TimeComparer.Compare(m.Value, max, timeComparison) < 0, MessageKey.Times.Between, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, DateTimeOffset min, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) >= 0 && TimeComparer.Compare(m, max, timeComparison) <= 0, MessageKey.Times.BetweenOrEqualTo, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, DateTimeOffset min, DateTimeOffset max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) >= 0 && TimeComparer.Compare(m.Value, max, timeComparison) <= 0, MessageKey.Times.BetweenOrEqualTo, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } } } ================================================ FILE: src/Validot/Rules/Times/DateTimeRules.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Translations; public static class DateTimeRules { public static IRuleOut EqualTo(this IRuleIn @this, DateTime value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, value, timeComparison) == 0, MessageKey.Times.EqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut EqualTo(this IRuleIn @this, DateTime value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, value, timeComparison) == 0, MessageKey.Times.EqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut NotEqualTo(this IRuleIn @this, DateTime value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, value, timeComparison) != 0, MessageKey.Times.NotEqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut NotEqualTo(this IRuleIn @this, DateTime value, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, value, timeComparison) != 0, MessageKey.Times.NotEqualTo, Arg.Time(nameof(value), value), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut After(this IRuleIn @this, DateTime min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) > 0, MessageKey.Times.After, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut After(this IRuleIn @this, DateTime min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) > 0, MessageKey.Times.After, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut AfterOrEqualTo(this IRuleIn @this, DateTime min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) >= 0, MessageKey.Times.AfterOrEqualTo, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut AfterOrEqualTo(this IRuleIn @this, DateTime min, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) >= 0, MessageKey.Times.AfterOrEqualTo, Arg.Time(nameof(min), min), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Before(this IRuleIn @this, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, max, timeComparison) < 0, MessageKey.Times.Before, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Before(this IRuleIn @this, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, max, timeComparison) < 0, MessageKey.Times.Before, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BeforeOrEqualTo(this IRuleIn @this, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, max, timeComparison) <= 0, MessageKey.Times.BeforeOrEqualTo, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BeforeOrEqualTo(this IRuleIn @this, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, max, timeComparison) <= 0, MessageKey.Times.BeforeOrEqualTo, Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Between(this IRuleIn @this, DateTime min, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) > 0 && TimeComparer.Compare(m, max, timeComparison) < 0, MessageKey.Times.Between, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut Between(this IRuleIn @this, DateTime min, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) > 0 && TimeComparer.Compare(m.Value, max, timeComparison) < 0, MessageKey.Times.Between, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, DateTime min, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m, min, timeComparison) >= 0 && TimeComparer.Compare(m, max, timeComparison) <= 0, MessageKey.Times.BetweenOrEqualTo, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } public static IRuleOut BetweenOrEqualTo(this IRuleIn @this, DateTime min, DateTime max, TimeComparison timeComparison = TimeComparison.All) { return @this.RuleTemplate(m => TimeComparer.Compare(m.Value, min, timeComparison) >= 0 && TimeComparer.Compare(m.Value, max, timeComparison) <= 0, MessageKey.Times.BetweenOrEqualTo, Arg.Time(nameof(min), min), Arg.Time(nameof(max), max), Arg.Enum(nameof(timeComparison), timeComparison)); } } } ================================================ FILE: src/Validot/Rules/Times/TimeComparer.cs ================================================ namespace Validot { using System; internal static class TimeComparer { public static int Compare(DateTime a, DateTime b, TimeComparison mode) { switch (mode) { case TimeComparison.JustDate: return DateTime.Compare(a.Date, b.Date); case TimeComparison.JustTime: return TimeSpan.Compare(a.TimeOfDay, b.TimeOfDay); case TimeComparison.All: default: return DateTime.Compare(a, b); } } public static int Compare(DateTimeOffset a, DateTimeOffset b, TimeComparison mode) { switch (mode) { case TimeComparison.JustDate: return DateTimeOffset.Compare(a.Date, b.Date); case TimeComparison.JustTime: return TimeSpan.Compare(a.TimeOfDay, b.TimeOfDay); case TimeComparison.All: default: return DateTimeOffset.Compare(a, b); } } } } ================================================ FILE: src/Validot/Rules/Times/TimeComparison.cs ================================================ namespace Validot { public enum TimeComparison { All = 0, JustDate = 1, JustTime = 2 } } ================================================ FILE: src/Validot/Settings/IValidatorSettings.cs ================================================ namespace Validot.Settings { using System.Collections.Generic; public interface IValidatorSettings { /// /// Gets translations dictionary - the key is the translation name, the value is the translation dictionary (where the key is the message key and the value is error message content for this key). /// IReadOnlyDictionary> Translations { get; } /// /// Gets a value indicating whether reference loop protection is enabled or not. If null, then it will be enabled automatically if the reference loop occurrence is theoretically possible (based on the specification). /// Reference loop protection is the mechanism that tracks self-references and prevents infinite loop traversing during the validation process. /// bool ReferenceLoopProtectionEnabled { get; } } } ================================================ FILE: src/Validot/Settings/ValidatorSettings.cs ================================================ namespace Validot.Settings { using System; using System.Collections.Generic; using Validot.Translations; /// /// Settings that uses to perform validations. /// public sealed class ValidatorSettings : IValidatorSettings { private readonly TranslationCompiler _translationCompiler = new TranslationCompiler(); internal ValidatorSettings() { } /// public IReadOnlyDictionary> Translations => _translationCompiler.Translations; /// public bool? ReferenceLoopProtectionEnabled { get; private set; } bool IValidatorSettings.ReferenceLoopProtectionEnabled => ReferenceLoopProtectionEnabled == true; internal bool IsLocked { get; set; } /// /// Gets the instance, initialized with the default values (English translation). /// /// with default values (English translation). public static ValidatorSettings GetDefault() { var settings = new ValidatorSettings().WithEnglishTranslation(); return settings; } /// /// Enables reference loop protection. It will be enabled automatically if the reference loop occurrence is theoretically possible (based on the specification). /// Reference loop protection is the mechanism that tracks self-references and prevents infinite loop traversing during the validation process. /// /// Settings fluent API builder - output. public ValidatorSettings WithReferenceLoopProtection() { ThrowIfLocked(); ReferenceLoopProtectionEnabled = true; return this; } /// /// Disables reference loop protection, even if the reference loop occurrence is theoretically possible (based on the specification). /// Reference loop protection is the mechanism that tracks self-references and prevents infinite loop traversing during the validation process. /// If the validated payloads are coming from untrustworthy sources, it might be dangerous to disable it. /// /// Settings fluent API builder - output. public ValidatorSettings WithReferenceLoopProtectionDisabled() { ThrowIfLocked(); ReferenceLoopProtectionEnabled = false; return this; } /// /// Adds translation entry for error messages. /// /// Translation name, e.g., "English". /// Message key. For custom messages - same content that is in WithMessage/WithExtraMessage. For built-in messages, find their keys in the docs (e.g., 'Texts.Email' for Email string rule). /// Translation content. /// Settings fluent API builder - output. public ValidatorSettings WithTranslation(string name, string messageKey, string translation) { ThrowIfLocked(); _translationCompiler.Add(name, messageKey, translation); return this; } private void ThrowIfLocked() { if (IsLocked) { throw new InvalidOperationException("Settings object is locked and can't be modified."); } } } } ================================================ FILE: src/Validot/Settings/ValidatorSettingsExtensions.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Settings; public static class ValidatorSettingsExtensions { /// /// Adds translation dictionary for error messages. /// /// Settings fluent API builder - input. /// Translation name, e.g., "English". /// Translation dictionary. The key is message key, the value is message content. /// Settings fluent API builder - output. public static ValidatorSettings WithTranslation(this ValidatorSettings @this, string name, IReadOnlyDictionary translation) { ThrowHelper.NullArgument(@this, nameof(@this)); ThrowHelper.NullArgument(name, nameof(name)); ThrowHelper.NullArgument(translation, nameof(translation)); foreach (var entry in translation) { _ = @this.WithTranslation(name, entry.Key, entry.Value); } return @this; } /// /// Adds translation dictionaries for error messages. /// /// Settings fluent API builder - input. /// Translation dictionaries. The key is the translation name, the value is another dictionary where the key is message key and the value is message content. /// Settings fluent API builder - output. public static ValidatorSettings WithTranslation(this ValidatorSettings @this, IReadOnlyDictionary> translations) { ThrowHelper.NullArgument(@this, nameof(@this)); ThrowHelper.NullArgument(translations, nameof(translations)); foreach (var translation in translations) { _ = @this.WithTranslation(translation.Key, translation.Value); } return @this; } } } ================================================ FILE: src/Validot/Specification/AndExtension.cs ================================================ namespace Validot { using Validot.Specification; public static class AndExtension { /// /// Contains no validation logic and no meaning. It's purpose is to visually separate rules in the fluent API method chain. /// /// Fluent API builder - input. /// Type of the specified model. /// Fluent API builder - output. public static IAndOut And(this IAndIn @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return (SpecificationApi)@this; } } namespace Specification { public interface IAndOut : IRuleIn { } public interface IAndIn { } internal partial class SpecificationApi : IAndIn, IAndOut { } } } ================================================ FILE: src/Validot/Specification/AsCollectionExtension.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; using Validot.Specification.Commands; public static class AsCollectionExtension { /// /// Validates every collection's item against the given specification. /// Each item's error output is saved under the path "#X", where X is the item's order within the collection. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// for the collection's items. /// Type of the collection. /// Type of the collection's item. /// Fluent API builder - output. public static IRuleOut AsCollection(this IRuleIn @this, Specification specification) where T : IEnumerable { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new AsCollectionCommand(specification)); } /// public static IRuleOut AsCollection(this IRuleIn @this, Specification specification) { return @this.AsCollection(specification); } /// public static IRuleOut> AsCollection(this IRuleIn> @this, Specification specification) { return @this.AsCollection, TItem>(specification); } /// public static IRuleOut> AsCollection(this IRuleIn> @this, Specification specification) { return @this.AsCollection, TItem>(specification); } /// public static IRuleOut> AsCollection(this IRuleIn> @this, Specification specification) { return @this.AsCollection, TItem>(specification); } /// public static IRuleOut> AsCollection(this IRuleIn> @this, Specification specification) { return @this.AsCollection, TItem>(specification); } /// public static IRuleOut> AsCollection(this IRuleIn> @this, Specification specification) { return @this.AsCollection, TItem>(specification); } /// public static IRuleOut> AsCollection(this IRuleIn> @this, Specification specification) { return @this.AsCollection, TItem>(specification); } } } ================================================ FILE: src/Validot/Specification/AsConvertedExtension.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Specification.Commands; public static class AsConvertedExtension { /// /// Converts the current scope value and validates it against the specification. The error output is saved in the current scope. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// A conversion function that takes the current scope value and outputs the new value. /// A specification for type used to validate the converted value. /// Type of the current scope value. /// Type of the converted value. /// Fluent API builder - output. public static IRuleOut AsConverted(this IRuleIn @this, Converter convert, Specification specification) { ThrowHelper.NullArgument(@this, nameof(@this)); ThrowHelper.NullArgument(convert, nameof(convert)); return ((SpecificationApi)@this).AddCommand(new AsConvertedCommand(convert, specification)); } } } ================================================ FILE: src/Validot/Specification/AsDictionaryExtension.cs ================================================ namespace Validot { using System; using System.Collections.Generic; using Validot.Specification; using Validot.Specification.Commands; public static class AsDictionaryExtension { /// /// Validates every value in the directory against the given specification. /// Each value's error output is saved under the path of this value's key in the dictionary. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// for the collection's items. /// Function that creates a string version of the dictionary's keys, so they can be used as a path segment in the results. /// Type of the dictionary. /// Type of the dictionary's key. /// Type of the dictionary's value. /// Fluent API builder - output. public static IRuleOut AsDictionary(this IRuleIn @this, Specification specification, Func keyStringifier) where T : IEnumerable> { ThrowHelper.NullArgument(@this, nameof(@this)); ThrowHelper.NullArgument(keyStringifier, nameof(keyStringifier)); return ((SpecificationApi)@this).AddCommand(new AsDictionaryCommand(specification, keyStringifier)); } /// public static IRuleOut>> AsDictionary(this IRuleIn>> @this, Specification specification, Func keyStringifier) { return @this.AsDictionary>, TKey, TValue>(specification, keyStringifier); } /// public static IRuleOut> AsDictionary(this IRuleIn> @this, Specification specification, Func keyStringifier) { return @this.AsDictionary, TKey, TValue>(specification, keyStringifier); } /// public static IRuleOut> AsDictionary(this IRuleIn> @this, Specification specification, Func keyStringifier) { return @this.AsDictionary, TKey, TValue>(specification, keyStringifier); } /// public static IRuleOut> AsDictionary(this IRuleIn> @this, Specification specification, Func keyStringifier) { return @this.AsDictionary, TKey, TValue>(specification, keyStringifier); } } } ================================================ FILE: src/Validot/Specification/AsDictionaryWithStringKeyExtension.cs ================================================ namespace Validot { using System.Collections.Generic; using Validot.Specification; using Validot.Specification.Commands; public static class AsDictionaryWithStringKeyExtension { /// /// Validates every value in the directory against the given specification. /// Each value's error output is saved under the path of this value's key in the dictionary. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// for the collection's items. /// Type of the dictionary. /// Type of the dictionary's value. /// Fluent API builder - output. public static IRuleOut AsDictionary(this IRuleIn @this, Specification specification) where T : IEnumerable> { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new AsDictionaryCommand(specification, null)); } /// public static IRuleOut>> AsDictionary(this IRuleIn>> @this, Specification specification) { return @this.AsDictionary>, TValue>(specification); } /// public static IRuleOut> AsDictionary(this IRuleIn> @this, Specification specification) { return @this.AsDictionary, TValue>(specification); } /// public static IRuleOut> AsDictionary(this IRuleIn> @this, Specification specification) { return @this.AsDictionary, TValue>(specification); } /// public static IRuleOut> AsDictionary(this IRuleIn> @this, Specification specification) { return @this.AsDictionary, TValue>(specification); } } } ================================================ FILE: src/Validot/Specification/AsModelExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class AsModelExtension { /// /// Validates the current scope value against the specification. The error output is saved in the current scope. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// Specification for the current scope value. /// Type of the current scope value. /// Fluent API builder - output. public static IRuleOut AsModel(this IRuleIn @this, Specification specification) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new AsModelCommand(specification)); } } } ================================================ FILE: src/Validot/Specification/AsNullableExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class AsNullableExtension { /// /// Validates the current scope nullable value against the specification. The error output is saved in the current scope. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// Specification for the value type wrapped in the nullable type. /// The value type wrapped in the nullable type. /// Fluent API builder - output. public static IRuleOut AsNullable(this IRuleIn @this, Specification specification) where T : struct { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new AsNullableCommand(specification)); } } } ================================================ FILE: src/Validot/Specification/AsTypeExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class AsTypeExtension { /// /// Casts the current scope value to another type and validates it against the specification. If the value can't be cast - nothing happens. The error output is saved in the current scope. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// A specification for type used to validate the value after casting. /// Type of the current scope value. /// Type that the value is cast to. /// Fluent API builder - output. public static IRuleOut AsType(this IRuleIn @this, Specification specification) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new AsTypeCommand(specification)); } } } ================================================ FILE: src/Validot/Specification/Commands/AsCollectionCommand.cs ================================================ namespace Validot.Specification.Commands { using System.Collections.Generic; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class AsCollectionCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class AsCollectionCommand : AsCollectionCommand where T : IEnumerable { public AsCollectionCommand(Specification specification) { ThrowHelper.NullArgument(specification, nameof(specification)); Specification = specification; } public Specification Specification { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (AsCollectionCommand)command; var block = new CollectionCommandScope { ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification) }; return block; }); } } } ================================================ FILE: src/Validot/Specification/Commands/AsConvertedCommand.cs ================================================ namespace Validot.Specification.Commands { using System; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class AsConvertedCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class AsConvertedCommand : AsConvertedCommand { public AsConvertedCommand(Converter converter, Specification specification) { ThrowHelper.NullArgument(converter, nameof(converter)); ThrowHelper.NullArgument(specification, nameof(specification)); Converter = converter; Specification = specification; } public Converter Converter { get; } public Specification Specification { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (AsConvertedCommand)command; var scope = new ConvertedCommandScope() { ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification), Converter = Converter, }; return scope; }); } } } ================================================ FILE: src/Validot/Specification/Commands/AsDictionaryCommand.cs ================================================ namespace Validot.Specification.Commands { using System; using System.Collections.Generic; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class AsDictionaryCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class AsDictionaryCommand : AsDictionaryCommand where T : IEnumerable> { public AsDictionaryCommand(Specification specification, Func keyStringifier) { ThrowHelper.NullArgument(specification, nameof(specification)); Specification = specification; KeyStringifier = keyStringifier; } public Specification Specification { get; } public Func KeyStringifier { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (AsDictionaryCommand)command; var scope = new DictionaryCommandScope { ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification), KeyStringifier = cmd.KeyStringifier, }; return scope; }); } } } ================================================ FILE: src/Validot/Specification/Commands/AsModelCommand.cs ================================================ namespace Validot.Specification.Commands { using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class AsModelCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class AsModelCommand : AsModelCommand { public AsModelCommand(Specification specification) { ThrowHelper.NullArgument(specification, nameof(specification)); Specification = specification; } public Specification Specification { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (AsModelCommand)command; var block = new ModelCommandScope { ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification) }; return block; }); } } } ================================================ FILE: src/Validot/Specification/Commands/AsNullableCommand.cs ================================================ namespace Validot.Specification.Commands { using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class AsNullableCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class AsNullableCommand : AsNullableCommand where T : struct { public AsNullableCommand(Specification specification) { ThrowHelper.NullArgument(specification, nameof(specification)); Specification = specification; } public Specification Specification { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (AsNullableCommand)command; var block = new NullableCommandScope { ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification) }; return block; }); } } } ================================================ FILE: src/Validot/Specification/Commands/AsTypeCommand.cs ================================================ namespace Validot.Specification.Commands { using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class AsTypeCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class AsTypeCommand : AsTypeCommand { public AsTypeCommand(Specification specification) { ThrowHelper.NullArgument(specification, nameof(specification)); Specification = specification; } public Specification Specification { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (AsTypeCommand)command; var scope = new TypeCommandScope() { ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification), }; return scope; }); } } } ================================================ FILE: src/Validot/Specification/Commands/ForbiddenCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal class ForbiddenCommand : ICommand { private static readonly Lazy LazyInstance = new Lazy(() => new ForbiddenCommand(), true); public static ForbiddenCommand Instance => LazyInstance.Value; } } ================================================ FILE: src/Validot/Specification/Commands/ICommand.cs ================================================ namespace Validot.Specification.Commands { internal interface ICommand { } } ================================================ FILE: src/Validot/Specification/Commands/IScopeCommand.cs ================================================ namespace Validot.Specification.Commands { using Validot.Validation.Scopes.Builders; internal interface IScopeCommand : ICommand { ICommandScopeBuilder GetScopeBuilder(); } } ================================================ FILE: src/Validot/Specification/Commands/MemberCommand.cs ================================================ namespace Validot.Specification.Commands { using System; using System.Linq; using System.Linq.Expressions; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal abstract class MemberCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class MemberCommand : MemberCommand { public MemberCommand(Expression> memberSelector, Specification specification) { ThrowHelper.NullArgument(memberSelector, nameof(memberSelector)); ThrowHelper.NullArgument(specification, nameof(specification)); MemberSelector = memberSelector; Specification = specification; } public Expression> MemberSelector { get; } public Specification Specification { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new CommandScopeBuilder(this, (command, context) => { var cmd = (MemberCommand)command; var block = new MemberCommandScope { GetMemberValue = cmd.MemberSelector.Compile(), ScopeId = context.GetOrRegisterSpecificationScope(cmd.Specification), Path = GetMemberName(cmd.MemberSelector) }; return block; }); } private static string GetMemberName(Expression> field) { if (field.ToString().Count(c => c == '.') > 1) { throw new InvalidOperationException($"Only one level of nesting is allowed, {field} looks like it is going further (member of a member?)"); } MemberExpression memberExpression = null; if (field.Body is MemberExpression) { memberExpression = (MemberExpression)field.Body; } else { throw new InvalidOperationException($"Only properties and variables are valid members to validate, {field} looks like it is pointing at something else (a method?)."); } return memberExpression.Member.Name; } } } ================================================ FILE: src/Validot/Specification/Commands/OptionalCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal class OptionalCommand : ICommand { private static readonly Lazy LazyInstance = new Lazy(() => new OptionalCommand(), true); public static OptionalCommand Instance => LazyInstance.Value; } } ================================================ FILE: src/Validot/Specification/Commands/RequiredCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal class RequiredCommand : ICommand { private static readonly Lazy LazyInstance = new Lazy(() => new RequiredCommand(), true); public static RequiredCommand Instance => LazyInstance.Value; } } ================================================ FILE: src/Validot/Specification/Commands/RuleCommand.cs ================================================ namespace Validot.Specification.Commands { using System; using System.Collections.Generic; using Validot.Errors.Args; using Validot.Validation.Scopes.Builders; internal abstract class RuleCommand : IScopeCommand { public abstract ICommandScopeBuilder GetScopeBuilder(); } internal class RuleCommand : RuleCommand { public RuleCommand(Predicate predicate) { ThrowHelper.NullArgument(predicate, nameof(predicate)); Predicate = predicate; } public RuleCommand(Predicate predicate, string message, IReadOnlyList args = null) : this(predicate) { ThrowHelper.NullArgument(message, nameof(message)); if (args != null) { ThrowHelper.NullInCollection(args, nameof(args)); } Message = message; Args = args; } public Predicate Predicate { get; } public string Message { get; } public IReadOnlyList Args { get; } public override ICommandScopeBuilder GetScopeBuilder() { return new RuleCommandScopeBuilder(this); } } } ================================================ FILE: src/Validot/Specification/Commands/WithCodeCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal class WithCodeCommand : ICommand { public WithCodeCommand(string code) { ThrowHelper.NullArgument(code, nameof(code)); if (!CodeHelper.IsCodeValid(code)) { throw new ArgumentException($"Invalid code: {code}", nameof(code)); } Code = code; } public string Code { get; } } } ================================================ FILE: src/Validot/Specification/Commands/WithConditionCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal abstract class WithConditionCommand : ICommand { } internal class WithConditionCommand : WithConditionCommand { public WithConditionCommand(Predicate executionCondition) { ThrowHelper.NullArgument(executionCondition, nameof(executionCondition)); ExecutionCondition = executionCondition; } public Predicate ExecutionCondition { get; } } } ================================================ FILE: src/Validot/Specification/Commands/WithExtraCodeCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal class WithExtraCodeCommand : ICommand { public WithExtraCodeCommand(string code) { ThrowHelper.NullArgument(code, nameof(code)); if (!CodeHelper.IsCodeValid(code)) { throw new ArgumentException($"Invalid code: {code}", nameof(code)); } Code = code; } public string Code { get; } } } ================================================ FILE: src/Validot/Specification/Commands/WithExtraMessageCommand.cs ================================================ namespace Validot.Specification.Commands { internal class WithExtraMessageCommand : ICommand { public WithExtraMessageCommand(string message) { ThrowHelper.NullArgument(message, nameof(message)); Message = message; } public string Message { get; } } } ================================================ FILE: src/Validot/Specification/Commands/WithMessageCommand.cs ================================================ namespace Validot.Specification.Commands { internal class WithMessageCommand : ICommand { public WithMessageCommand(string message) { ThrowHelper.NullArgument(message, nameof(message)); Message = message; } public string Message { get; } } } ================================================ FILE: src/Validot/Specification/Commands/WithPathCommand.cs ================================================ namespace Validot.Specification.Commands { using System; internal class WithPathCommand : ICommand { public WithPathCommand(string path) { ThrowHelper.NullArgument(path, nameof(path)); if (!PathHelper.IsValidAsPath(path)) { throw new ArgumentException("Invalid path", nameof(path)); } Path = path; } public string Path { get; } } } ================================================ FILE: src/Validot/Specification/ForbiddenExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class ForbiddenExtension { /// /// Forbids the current scope to have a value - only null is valid. In case of non-null value, an error is saved in the current scope. /// This is a presence command - must be at the beginning, error output can be altered by following parameter commands: WithMessage, WithExtraMessage, WithCode, WithExtraCode. /// /// Fluent API builder - input. /// Type of the current scope value. /// Fluent API builder - output. public static IForbiddenOut Forbidden(this IForbiddenIn @this) where T : class { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(ForbiddenCommand.Instance); } /// public static IForbiddenOut Forbidden(this IForbiddenIn @this) where T : struct { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(ForbiddenCommand.Instance); } } namespace Specification { public interface IForbiddenOut : ISpecificationOut, IWithMessageForbiddenIn, IWithExtraMessageForbiddenIn, IWithCodeForbiddenIn, IWithExtraCodeForbiddenIn { } public interface IForbiddenIn { } public partial interface ISpecificationIn : IForbiddenIn { } internal partial class SpecificationApi : IForbiddenIn, IForbiddenOut { } } } ================================================ FILE: src/Validot/Specification/MemberExtension.cs ================================================ namespace Validot { using System; using System.Linq.Expressions; using Validot.Specification; using Validot.Specification.Commands; public static class MemberExtension { /// /// Validates the current scope value's member against the specification. The error output is saved under the path derived from the member's name. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// Expression that points at the current scope's direct member. Only one step down is allowed. /// Specification for the current scope value's member. /// Type of the current scope value. /// Type of the current scope value's member. /// Fluent API builder - output. public static IRuleOut Member(this IRuleIn @this, Expression> memberSelector, Specification specification) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new MemberCommand(memberSelector, specification)); } } } ================================================ FILE: src/Validot/Specification/OptionalExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class OptionalExtension { /// /// Allows the current scope value to have null. In case of null, no error is saved. /// This is a presence command - must be at the beginning. /// /// Fluent API builder - input. /// Type of the current scope value. /// Fluent API builder - output. public static IOptionalOut Optional(this IOptionalIn @this) where T : class { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(OptionalCommand.Instance); } /// public static IOptionalOut Optional(this IOptionalIn @this) where T : struct { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(OptionalCommand.Instance); } } namespace Specification { public interface IOptionalOut : ISpecificationOut, IRuleIn, IAndIn { } public interface IOptionalIn { } public partial interface ISpecificationIn : IOptionalIn { } internal partial class SpecificationApi : IOptionalIn, IOptionalOut { } } } ================================================ FILE: src/Validot/Specification/RequiredExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class RequiredExtension { /// /// Forbids the current scope value to have null. In case of null, an error is saved in the current scope. /// All values are required by default - this command allows to modify the error output in case of null value. /// This is a presence command - must be at the beginning, error output can be altered by following parameter commands: WithMessage, WithExtraMessage, WithCode, WithExtraCode. /// /// Fluent API builder - input. /// Type of the current scope value. /// Fluent API builder - output. public static IRequiredOut Required(this IRequiredIn @this) where T : class { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(RequiredCommand.Instance); } /// public static IRequiredOut Required(this IRequiredIn @this) where T : struct { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(RequiredCommand.Instance); } } namespace Specification { public interface IRequiredOut : ISpecificationOut, IRuleIn, IWithMessageIn, IWithExtraMessageIn, IWithCodeIn, IWithExtraCodeIn, IAndIn { } public interface IRequiredIn { } public partial interface ISpecificationIn : IRequiredIn { } internal partial class SpecificationApi : IRequiredIn, IRequiredOut { } } } ================================================ FILE: src/Validot/Specification/RuleExtension.cs ================================================ namespace Validot { using System; using Validot.Errors.Args; using Validot.Specification; using Validot.Specification.Commands; public static class RuleExtension { /// /// Validates the current scope value with a custom logic wrapped in the predicate. Error output is saved in the current scope. /// To specify the error output (messages, codes, etc.), follow this commands with parameters commands like WithMessage, WithExtraMessage, WithCode, WithExtraCode. /// This is a scope command - its error output can be altered with any of the parameter commands (WithCondition, WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode). /// /// Fluent API builder - input. /// Predicate that takes the current scope value and returns true if its valid, false - if invalid. /// Type of the current scope value. /// Fluent API builder - output. public static IRuleOut Rule(this IRuleIn @this, Predicate predicate) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new RuleCommand(predicate)); } /// /// This command shouldn't be used in the specification. It's primarily for creating predefined rules. /// Find "Custom Rules" section in the documentation. /// /// Fluent API builder - input. /// Predicate that takes the current scope value and returns true if its valid, false - if invalid. /// Message key (same meaning as in ). /// Message args (same meaning as in ). /// Type of the current scope value. /// Fluent API builder - output. public static IRuleOut RuleTemplate(this IRuleIn @this, Predicate predicate, string key, params IArg[] args) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new RuleCommand(predicate, key, args)); } } namespace Specification { public interface IRuleOut : ISpecificationOut, IRuleIn, IWithConditionIn, IWithPathIn, IWithMessageIn, IWithExtraMessageIn, IWithCodeIn, IWithExtraCodeIn, IAndIn { } public interface IRuleIn { } public partial interface ISpecificationIn : IRuleIn { } internal partial class SpecificationApi : IRuleIn, IRuleOut { } } } ================================================ FILE: src/Validot/Specification/Specification.cs ================================================ namespace Validot { using Validot.Specification; /// /// Specification describes the valid state of an object using fluent interface. /// /// Fluent API builder - input. /// Type of the specified model. /// Fluent API builder - output. public delegate ISpecificationOut Specification(ISpecificationIn api); } ================================================ FILE: src/Validot/Specification/SpecificationApi.cs ================================================ namespace Validot.Specification { using System.Collections.Generic; using Validot.Specification.Commands; public partial interface ISpecificationIn : ISpecificationOut { } public interface ISpecificationOut { } internal partial class SpecificationApi : ISpecificationIn { private readonly List _commands = new List(); public IReadOnlyList Commands => _commands; public SpecificationApi AddCommand(ICommand command) { ThrowHelper.NullArgument(command, nameof(command)); _commands.Add(command); return this; } } } ================================================ FILE: src/Validot/Specification/WithCodeExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class WithCodeExtension { /// /// Overwrites the entire error output (of the closest preceding scope command) with a single error code. /// This is a parameter command - it can be followed by a new scope command or other parameter commands: WithExtraCode. /// /// Fluent API builder - input. /// Error code to be saved in the error output in case the closest preceding scope command indicates invalid value. /// Type of the current scope value. /// Fluent API builder - output. public static IWithCodeOut WithCode(this IWithCodeIn @this, string code) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithCodeCommand(code)); } /// public static IWithCodeForbiddenOut WithCode(this IWithCodeForbiddenIn @this, string code) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithCodeCommand(code)); } } namespace Specification { public interface IWithCodeOut : ISpecificationOut, IRuleIn, IWithExtraCodeIn, IAndIn { } public interface IWithCodeIn { } public interface IWithCodeForbiddenOut : ISpecificationOut, IWithExtraCodeForbiddenIn { } public interface IWithCodeForbiddenIn { } internal partial class SpecificationApi : IWithCodeIn, IWithCodeOut, IWithCodeForbiddenIn, IWithCodeForbiddenOut { } } } ================================================ FILE: src/Validot/Specification/WithConditionExtension.cs ================================================ namespace Validot { using System; using Validot.Specification; using Validot.Specification.Commands; public static class WithConditionExtension { /// /// Sets execution condition of the closest preceding scope command. If the condition is not met, the validation logic is not even executed. /// This is a parameter command - it can be followed by a new scope command or other parameter commands: WithPath, WithMessage, WithExtraMessage, WithCode, WithExtraCode. /// /// Fluent API builder - input. /// Execution condition of the closest preceding scope command. A predicate that receives the current scope's value and determines whether the validation logic should be executed (returns true if yes, otherwise - false). /// Type of the current scope value. /// Fluent API builder - output. public static IWithConditionOut WithCondition(this IWithConditionIn @this, Predicate executionCondition) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithConditionCommand(executionCondition)); } } namespace Specification { public interface IWithConditionOut : ISpecificationOut, IRuleIn, IWithPathIn, IWithMessageIn, IWithExtraMessageIn, IWithCodeIn, IWithExtraCodeIn, IAndIn { } public interface IWithConditionIn { } internal partial class SpecificationApi : IWithConditionIn, IWithConditionOut { } } } ================================================ FILE: src/Validot/Specification/WithExtraCodeExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class WithExtraCodeExtension { /// /// Appends error code to the error output (of the closest preceding scope command). /// This is a parameter command - it can be followed by a new scope command or other parameter commands: WithExtraCode. /// /// Fluent API builder - input. /// Error code to be saved in the error output in case the closest preceding scope command indicates invalid value. /// Type of the current scope value. /// Fluent API builder - output. public static IWithExtraCodeOut WithExtraCode(this IWithExtraCodeIn @this, string code) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithExtraCodeCommand(code)); } /// public static IWithExtraCodeForbiddenOut WithExtraCode(this IWithExtraCodeForbiddenIn @this, string code) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithExtraCodeCommand(code)); } } namespace Specification { public interface IWithExtraCodeOut : ISpecificationOut, IRuleIn, IWithExtraCodeIn, IAndIn { } public interface IWithExtraCodeIn { } public interface IWithExtraCodeForbiddenOut : ISpecificationOut, IWithExtraCodeForbiddenIn { } public interface IWithExtraCodeForbiddenIn { } internal partial class SpecificationApi : IWithExtraCodeIn, IWithExtraCodeOut, IWithExtraCodeForbiddenIn, IWithExtraCodeForbiddenOut { } } } ================================================ FILE: src/Validot/Specification/WithExtraMessageExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class WithExtraMessageExtension { /// /// Appends error message to the error output (of the closest preceding scope command). /// This is a parameter command - it can be followed by a new scope command or other parameter commands: WithExtraMessage, WithExtraCode. /// /// Fluent API builder - input. /// Error message to be saved in the error output in case the closest preceding scope command indicates invalid value. This is also a message key that could be replaced by the translation dictionary. /// Type of the current scope value. /// Fluent API builder - output. public static IWithExtraMessageOut WithExtraMessage(this IWithExtraMessageIn @this, string message) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithExtraMessageCommand(message)); } /// public static IWithExtraMessageForbiddenOut WithExtraMessage(this IWithExtraMessageForbiddenIn @this, string message) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithExtraMessageCommand(message)); } } namespace Specification { public interface IWithExtraMessageOut : ISpecificationOut, IRuleIn, IWithExtraMessageIn, IWithExtraCodeIn, IAndIn { } public interface IWithExtraMessageIn { } public interface IWithExtraMessageForbiddenOut : ISpecificationOut, IWithExtraMessageForbiddenIn, IWithExtraCodeForbiddenIn { } public interface IWithExtraMessageForbiddenIn { } internal partial class SpecificationApi : IWithExtraMessageIn, IWithExtraMessageOut, IWithExtraMessageForbiddenIn, IWithExtraMessageForbiddenOut { } } } ================================================ FILE: src/Validot/Specification/WithMessageExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class WithMessageExtension { /// /// Overwrites the entire error output (of the closest preceding scope command) with a single error message. /// This is a parameter command - it can be followed by a new scope command or other parameter commands: WithExtraMessage, WithExtraCode. /// /// Fluent API builder - input. /// Error message to be saved in the error output in case the closest preceding scope command indicates invalid value. This is also a message key that could be replaced by the translation dictionary. /// Type of the current scope value. /// Fluent API builder - output. public static IWithMessageOut WithMessage(this IWithMessageIn @this, string message) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithMessageCommand(message)); } /// public static IWithMessageForbiddenOut WithMessage(this IWithMessageForbiddenIn @this, string message) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithMessageCommand(message)); } } namespace Specification { public interface IWithMessageOut : ISpecificationOut, IRuleIn, IWithExtraMessageIn, IWithExtraCodeIn, IAndIn { } public interface IWithMessageIn { } public interface IWithMessageForbiddenOut : ISpecificationOut, IWithExtraMessageForbiddenIn, IWithExtraCodeForbiddenIn { } public interface IWithMessageForbiddenIn { } internal partial class SpecificationApi : IWithMessageIn, IWithMessageOut, IWithMessageForbiddenIn, IWithMessageForbiddenOut { } } } ================================================ FILE: src/Validot/Specification/WithPathExtension.cs ================================================ namespace Validot { using Validot.Specification; using Validot.Specification.Commands; public static class WithPathExtension { /// /// Overwrites the path of the current scope's error output. /// This is a parameter command - it can be followed by a new scope command or other parameter commands: WithMessage, WithExtraMessage, WithCode, WithExtraCode. /// /// Fluent API builder - input. /// New path of the current scope's error output. To nest it more deeply, simply use dots (e.g., "More.Nested.Level"). To move it to the upper level, use `<` character (e.g., "<", "<<TwoLevelsUp.And.ThreeDown"). /// Type of the current scope value. /// Fluent API builder - output. public static IWithPathOut WithPath(this IWithPathIn @this, string path) { ThrowHelper.NullArgument(@this, nameof(@this)); return ((SpecificationApi)@this).AddCommand(new WithPathCommand(path)); } } namespace Specification { public interface IWithPathOut : ISpecificationOut, IRuleIn, IWithMessageIn, IWithExtraMessageIn, IWithCodeIn, IWithExtraCodeIn, IAndIn { } public interface IWithPathIn { } internal partial class SpecificationApi : IWithPathIn, IWithPathOut { } } } ================================================ FILE: src/Validot/Testing/TestFailedException.cs ================================================ namespace Validot.Testing { public sealed class TestFailedException : ValidotException { public TestFailedException(string message) : base(message) { } } } ================================================ FILE: src/Validot/Testing/TestResult.cs ================================================ namespace Validot.Testing { public sealed class TestResult { private static readonly TestResult SuccessResult = new TestResult(null); private TestResult(string message) { Message = message; } public bool Success => Message == null; public string Message { get; } public static TestResult Passed() { return SuccessResult; } public static TestResult Failed(string message) { return new TestResult(message); } public void ThrowExceptionIfFailed() { if (!Success) { throw new TestFailedException(Message); } } } } ================================================ FILE: src/Validot/Testing/Tester.cs ================================================ namespace Validot.Testing { using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Validot.Errors; using Validot.Errors.Args; using Validot.Results; public static class Tester { public static TestResult TestSpecification(T value, Specification specification, IReadOnlyDictionary> expectedErrors = null) { var validator = Validator.Factory.Create(specification); var result = (ValidationResult)validator.Validate(value); var shouldBeValid = expectedErrors == null; if (!result.AnyErrors && shouldBeValid) { return TestResult.Passed(); } if (result.AnyErrors == shouldBeValid) { return TestResult.Failed($"Expected result IsValid: {shouldBeValid}, but AnyErrors: {result.AnyErrors}"); } var errors = result.GetErrorOutput(); if (expectedErrors == null) { ThrowHelper.Fatal("By this point all of the checks in TestSpecification method should prevent this logical path."); return TestResult.Failed(null); } if (errors.Count != expectedErrors.Count) { return TestResult.Failed($"Expected amount of paths with errors: {expectedErrors.Count}, but found: {errors.Count}"); } var missingPath = expectedErrors.Keys.FirstOrDefault(expectedKey => { return errors.Keys.All(key => !string.Equals(expectedKey, key, StringComparison.Ordinal)); }); if (missingPath != null) { return TestResult.Failed($"Expected error path is missing: `{missingPath}`"); } foreach (var errorPair in errors) { var path = errorPair.Key; var pathErrors = errorPair.Value; var pathExpectedErrors = expectedErrors[path]; if (pathErrors.Count != pathExpectedErrors.Count) { return TestResult.Failed($"Expected errors amount (for path `{path}`): {pathExpectedErrors.Count}, but found {pathErrors.Count}"); } for (var j = 0; j < pathErrors.Count; ++j) { var error = pathErrors.ElementAt(j); var expectedError = pathExpectedErrors.ElementAt(j); var expectedErrorMessagesCount = expectedError.Messages?.Count ?? 0; if (error.Messages.Count != expectedErrorMessagesCount) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) messages amount to be {expectedErrorMessagesCount}, but found {error.Messages.Count}"); } for (var k = 0; k < error.Messages.Count; ++k) { var errorMessage = error.Messages.ElementAt(k); var expectedErrorMessage = expectedError.Messages.ElementAt(k); if (!string.Equals(errorMessage, expectedErrorMessage, StringComparison.Ordinal)) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) message (index {k}) to be `{expectedErrorMessage}`, but found `{errorMessage}`"); } } var expectedErrorCodesCount = expectedError.Codes?.Count ?? 0; if (error.Codes.Count != expectedErrorCodesCount) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) codes amount to be {expectedErrorCodesCount}, but found {error.Codes.Count}"); } for (var k = 0; k < error.Codes.Count; ++k) { var errorCode = error.Codes.ElementAt(k); var expectedErrorCode = expectedError.Codes.ElementAt(k); if (!string.Equals(errorCode, expectedErrorCode, StringComparison.Ordinal)) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) code (index {k}) to be `{expectedErrorCode}`, but found `{errorCode}`"); } } var expectedErrorArgsCount = expectedError.Args?.Count ?? 0; if (error.Args.Count != expectedErrorArgsCount) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) args amount to be {expectedErrorArgsCount}, but found {error.Args.Count}"); } if (expectedErrorArgsCount == 0) { continue; } var missingArg = expectedError.Args.FirstOrDefault(a => { return error.Args.All(ea => !string.Equals(a.Name, ea.Name, StringComparison.Ordinal)); }); if (missingArg != null) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) arg is missing: `{missingArg.Name}`"); } foreach (var errorArg in error.Args) { var expectedErrorArg = expectedError.Args.Single(a => a.Name == errorArg.Name); var argType = errorArg.GetType(); var expectedArgType = expectedErrorArg.GetType(); if (!expectedArgType.IsAssignableFrom(argType)) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) arg (name `{errorArg.Name}`) type to be `{expectedArgType.GetFriendlyName(true)}`, but found `{argType.GetFriendlyName(true)}`"); } var argValue = argType.GetProperties().Single(p => p.Name == "Value").GetValue(errorArg); var expectedArgValue = argType.GetProperties().Single(p => p.Name == "Value").GetValue(expectedErrorArg); if (argValue is double d) { if (Math.Abs(d - (double)expectedArgValue) > 0.0000001d) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) arg (name `{errorArg.Name}`) double value to be `{((double)expectedArgValue).ToString(CultureInfo.InvariantCulture)}`, but found `{((double)argValue).ToString(CultureInfo.InvariantCulture)}`"); } } else if (argValue is float f) { if (Math.Abs(f - (float)expectedArgValue) > 0.0000001f) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) arg (name `{errorArg.Name}`) float value to be `{((float)expectedArgValue).ToString(CultureInfo.InvariantCulture)}`, but found `{((float)argValue).ToString(CultureInfo.InvariantCulture)}`"); } } else if (!expectedArgValue.Equals(argValue)) { return TestResult.Failed($"Expected error (for path `{path}`, index {j}) arg (name `{errorArg.Name}`) value to be `{expectedArgValue}`, but found `{argValue}`"); } } } } return TestResult.Passed(); } public static void TestSingleRule(T value, Specification specification, bool shouldBeValid, string message = null, params IArg[] args) { if (message == null && args?.Any() == true) { throw new ArgumentException($"{nameof(message)} cannot be null if {nameof(args)} is non-null", nameof(message)); } var expectedErrors = shouldBeValid ? null : new Dictionary> { [string.Empty] = new[] { new Error { Messages = new[] { message }, Args = args, Codes = Array.Empty() } } }; TestSpecification(value, specification, expectedErrors).ThrowExceptionIfFailed(); } public static Exception TestExceptionOnInit(Specification specification, Type expectedException) { ThrowHelper.NullArgument(specification, nameof(specification)); ThrowHelper.NullArgument(expectedException, nameof(expectedException)); try { _ = Validator.Factory.Create(specification); } catch (Exception exception) { if (!expectedException.IsInstanceOfType(exception)) { throw new TestFailedException($"Exception of type {expectedException.FullName} was expected, but found {exception.GetType().FullName}."); } return exception; } throw new TestFailedException($"Exception of type {expectedException.FullName} was expected, but no exception has been thrown."); } public static TestResult TestResultToString(string toStringOutput, ToStringContentType toStringContentType, params string[] expectedLines) { ThrowHelper.NullArgument(toStringOutput, nameof(toStringOutput)); ThrowHelper.NullArgument(expectedLines, nameof(expectedLines)); if (expectedLines.Length == 0) { throw new ArgumentException("Empty list of expected lines", nameof(expectedLines)); } if (toStringContentType == ToStringContentType.Codes) { if (expectedLines.Length != 1) { throw new ArgumentException($"Expected codes only (all in the single line), but found lines: {expectedLines.Length}", nameof(expectedLines)); } } if (toStringContentType == ToStringContentType.MessagesAndCodes) { if (expectedLines.Length < 3) { throw new ArgumentException($"Expected codes and messages (so at least 3 lines), but found lines: {expectedLines.Length}", nameof(expectedLines)); } if (!string.IsNullOrEmpty(expectedLines[1])) { throw new ArgumentException($"Expected codes and messages (divided by a single empty line), but found in second line: {expectedLines[1]}", nameof(expectedLines)); } if (expectedLines.Skip(2).Any(string.IsNullOrEmpty)) { throw new ArgumentException($"Expected codes and messages (divided by a single empty line), also another empty line", nameof(expectedLines)); } } if (toStringContentType == ToStringContentType.Messages) { if (expectedLines.Any(string.IsNullOrEmpty)) { throw new ArgumentException($"Expected messages only, but found empty line", nameof(expectedLines)); } } var hasCodes = toStringContentType == ToStringContentType.Codes || toStringContentType == ToStringContentType.MessagesAndCodes; var lines = toStringOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.None); if (lines.Length != expectedLines.Length) { return TestResult.Failed($"Expected amount of lines: {expectedLines.Length}, but found: {lines.Length}"); } if (hasCodes) { var codes = lines[0].Split(new[] { ", " }, StringSplitOptions.None); var expectedCodes = expectedLines[0].Split(new[] { ", " }, StringSplitOptions.None); var missingCodes = expectedCodes.Where(expectedCode => codes.All(c => !string.Equals(c, expectedCode, StringComparison.Ordinal))).OrderBy(a => a).ToArray(); if (missingCodes.Any()) { return TestResult.Failed($"Expected codes that are missing: {string.Join(", ", missingCodes)}"); } if (codes.Length != expectedCodes.Length) { return TestResult.Failed($"Expected amount of codes: {expectedCodes.Length}, but found: {codes.Length}"); } } if (toStringContentType == ToStringContentType.MessagesAndCodes) { if (!string.IsNullOrEmpty(lines[1])) { return TestResult.Failed($"Expected codes and messages (divided by a single line), but found in second line: {lines[1]}"); } } var messageLines = toStringContentType == ToStringContentType.Messages ? lines : lines.Skip(2).ToArray(); var expectedMessageLines = toStringContentType == ToStringContentType.Messages ? expectedLines : expectedLines.Skip(2).ToArray(); var missingMessages = expectedMessageLines.Where(expectedMessageLine => messageLines.All(c => !string.Equals(c, expectedMessageLine, StringComparison.Ordinal))).ToArray(); if (missingMessages.Any()) { return TestResult.Failed($"Expected messages that are missing: {string.Join(", ", missingMessages.OrderBy(s => s).Select(s => $"`{s}`"))}"); } return TestResult.Passed(); } public static void ShouldResultToStringHaveLines(this string @this, ToStringContentType toStringContentType, params string[] expectedLines) { TestResultToString(@this, toStringContentType, expectedLines).ThrowExceptionIfFailed(); } } } ================================================ FILE: src/Validot/Testing/ToStringContentType.cs ================================================ namespace Validot.Testing { public enum ToStringContentType { Messages = 0, Codes = 1, MessagesAndCodes = 3 } } ================================================ FILE: src/Validot/ThrowHelper.cs ================================================ namespace Validot { using System; using System.Collections.Generic; using System.Linq; internal static class ThrowHelper { public static void Fatal(string message) { throw new ValidotException($"Oooops! Sorry! That shouldn't have happened! {Environment.NewLine}{message}{Environment.NewLine} Please raise the issue on github with this exception stacktrace and details."); } public static void NullArgument(T argument, string name) where T : class { if (argument == null) { throw new ArgumentNullException(name); } } public static void NullInCollection(IReadOnlyList collection, string name) where T : class { NullArgument(collection, name); for (var i = 0; i < collection.Count; ++i) { if (collection.ElementAt(i) == null) { throw new ArgumentNullException($"Collection `{name}` contains null under index{i}"); } } } public static void NullInCollection(IEnumerable collection, string name) where T : class { NullArgument(collection, name); foreach (var item in collection) { if (item == null) { throw new ArgumentNullException($"Collection `{name}` contains null"); } } } public static void BelowZero(int argument, string name) { if (argument < 0) { throw new ArgumentOutOfRangeException(name, argument, $"{name} cannot be less than zero"); } } public static void InvalidRange(long minArgument, string minName, long maxArgument, string maxName) { if (minArgument > maxArgument) { throw new ArgumentException($"{minName} (value: {minArgument}) cannot be above {maxName} (value: {maxArgument})"); } } public static void InvalidRange(ulong minArgument, string minName, ulong maxArgument, string maxName) { if (minArgument > maxArgument) { throw new ArgumentException($"{minName} (value: {minArgument}) cannot be above {maxName} (value: {maxArgument})"); } } public static void InvalidRange(decimal minArgument, string minName, decimal maxArgument, string maxName) { if (minArgument > maxArgument) { throw new ArgumentException($"{minName} (value: {minArgument}) cannot be above {maxName} (value: {maxArgument})"); } } public static void InvalidRange(double minArgument, string minName, double maxArgument, string maxName) { if (minArgument > maxArgument) { throw new ArgumentException($"{minName} (value: {minArgument}) cannot be above {maxName} (value: {maxArgument})"); } } } } ================================================ FILE: src/Validot/Translations/Chinese/ChineseTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary Chinese { get; } = new Dictionary { [MessageKey.Global.Error] = "错误", [MessageKey.Global.Required] = "需要", [MessageKey.Global.Forbidden] = "禁止", [MessageKey.Global.ReferenceLoop] = "(引用循环)", [MessageKey.BoolType.True] = "必须为真", [MessageKey.BoolType.False] = "必须为假", [MessageKey.CharType.EqualToIgnoreCase] = "必须等于 {value} (无视条件)", [MessageKey.CharType.NotEqualToIgnoreCase] = "不可等于 {value} (无视条件)", [MessageKey.GuidType.EqualTo] = "必须等于 {value}", [MessageKey.GuidType.NotEqualTo] = "不可等于 {value}", [MessageKey.GuidType.NotEmpty] = "不可全部为零", [MessageKey.Collections.EmptyCollection] = "必须为空", [MessageKey.Collections.NotEmptyCollection] = "不可为空", [MessageKey.Collections.ExactCollectionSize] = "必须包含 {size} 个对象", [MessageKey.Collections.MinCollectionSize] = "必须包含至少 {min} 个对象", [MessageKey.Collections.MaxCollectionSize] = "只能包含最多 {max} 个对象", [MessageKey.Collections.CollectionSizeBetween] = "只能包含 {min} 到 {max} 个对象", [MessageKey.Numbers.EqualTo] = "必须等于 {value}", [MessageKey.Numbers.NotEqualTo] = "不可等于 {value}", [MessageKey.Numbers.GreaterThan] = "必须大于 {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "必须大于或等于 {min}", [MessageKey.Numbers.LessThan] = "只能小于 {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "只能小于或等于 {max}", [MessageKey.Numbers.Between] = "只能在 {min} 到 {max} 之间 (不包括)", [MessageKey.Numbers.BetweenOrEqualTo] = "只能在 {min} 到 {max} 之间 (包括)", [MessageKey.Numbers.NonZero] = "不可为零", [MessageKey.Numbers.Positive] = "必须为正值", [MessageKey.Numbers.NonPositive] = "不可为正值", [MessageKey.Numbers.Negative] = "必须为负值", [MessageKey.Numbers.NonNegative] = "不可为负值", [MessageKey.Numbers.NonNaN] = "必须非NaN", [MessageKey.Texts.Email] = "必须为有效邮箱地址", [MessageKey.Texts.EqualTo] = "必须等于 {value}", [MessageKey.Texts.NotEqualTo] = "不可等于 {value}", [MessageKey.Texts.Contains] = "必须包含 {value}", [MessageKey.Texts.NotContains] = "不可包含 {value}", [MessageKey.Texts.NotEmpty] = "不可为空", [MessageKey.Texts.NotWhiteSpace] = "不可仅使用空格字符", [MessageKey.Texts.SingleLine] = "只能包含单行", [MessageKey.Texts.ExactLength] = "长度必须确切为 {length} 个字符", [MessageKey.Texts.MaxLength] = "长度最多为 {max} 个字符", [MessageKey.Texts.MinLength] = "长度最少为 {min} 个字符", [MessageKey.Texts.LengthBetween] = "长度只能在 {min} 到 {max} 个字符之间", [MessageKey.Texts.Matches] = "必须匹配正则表达式模板 {pattern}", [MessageKey.Texts.StartsWith] = "必须以 {value} 起始", [MessageKey.Texts.EndsWith] = "必须以 {value} 结束", [MessageKey.Times.EqualTo] = "必须等于 {value}", [MessageKey.Times.NotEqualTo] = "不可等于 {value}", [MessageKey.Times.After] = "必须晚于 {min}", [MessageKey.Times.AfterOrEqualTo] = "必须晚于或等于 {min}", [MessageKey.Times.Before] = "必须早于 {max}", [MessageKey.Times.BeforeOrEqualTo] = "必须早于或等于 {max}", [MessageKey.Times.Between] = "只能处于 {min} 到 {max} 之间 (不包括)", [MessageKey.Times.BetweenOrEqualTo] = "只能处于 {min} 到 {max} 之间 (包括)", [MessageKey.TimeSpanType.EqualTo] = "必须等于 {value}", [MessageKey.TimeSpanType.NotEqualTo] = "不可等于 {value}", [MessageKey.TimeSpanType.GreaterThan] = "必须大于 {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "必须大于或等于 {min}", [MessageKey.TimeSpanType.LessThan] = "只能小于 {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "只能小于或等于 {max}", [MessageKey.TimeSpanType.Between] = "只能处于 {min} 到 {max} 之间 (不包括)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "只能处于 {min} 到 {max} 之间 (包括)", [MessageKey.TimeSpanType.NonZero] = "不可为零", [MessageKey.TimeSpanType.Positive] = "必须为正值", [MessageKey.TimeSpanType.NonPositive] = "不可为正值", [MessageKey.TimeSpanType.Negative] = "必须为负值", [MessageKey.TimeSpanType.NonNegative] = "不可为负值" }; } } ================================================ FILE: src/Validot/Translations/Chinese/ChineseTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds Chinese translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithChineseTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.Chinese), Translation.Chinese); } } } ================================================ FILE: src/Validot/Translations/English/EnglishTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary English { get; } = new Dictionary { [MessageKey.Global.Error] = "Error", [MessageKey.Global.Required] = "Required", [MessageKey.Global.Forbidden] = "Forbidden", [MessageKey.Global.ReferenceLoop] = "(reference loop)", [MessageKey.BoolType.True] = "Must be true", [MessageKey.BoolType.False] = "Must be false", [MessageKey.CharType.EqualToIgnoreCase] = "Must be equal to {value} (ignoring case)", [MessageKey.CharType.NotEqualToIgnoreCase] = "Must not be equal to {value} (ignoring case)", [MessageKey.GuidType.EqualTo] = "Must be equal to {value}", [MessageKey.GuidType.NotEqualTo] = "Must not be equal to {value}", [MessageKey.GuidType.NotEmpty] = "Must not be all zeros", [MessageKey.Collections.EmptyCollection] = "Must be empty", [MessageKey.Collections.NotEmptyCollection] = "Must not be empty", [MessageKey.Collections.ExactCollectionSize] = "Must contain exactly {size} items", [MessageKey.Collections.MinCollectionSize] = "Must contain at least {min} items", [MessageKey.Collections.MaxCollectionSize] = "Must contain at most {max} items", [MessageKey.Collections.CollectionSizeBetween] = "Must contain between {min} and {max} items", [MessageKey.Numbers.EqualTo] = "Must be equal to {value}", [MessageKey.Numbers.NotEqualTo] = "Must not be equal to {value}", [MessageKey.Numbers.GreaterThan] = "Must be greater than {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Must be greater than or equal to {min}", [MessageKey.Numbers.LessThan] = "Must be less than {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Must be less than or equal to {max}", [MessageKey.Numbers.Between] = "Must be between {min} and {max} (exclusive)", [MessageKey.Numbers.BetweenOrEqualTo] = "Must be between {min} and {max} (inclusive)", [MessageKey.Numbers.NonZero] = "Must not be zero", [MessageKey.Numbers.Positive] = "Must be positive", [MessageKey.Numbers.NonPositive] = "Must not be positive", [MessageKey.Numbers.Negative] = "Must be negative", [MessageKey.Numbers.NonNegative] = "Must not be negative", [MessageKey.Numbers.NonNaN] = "Must not be NaN", [MessageKey.Texts.Email] = "Must be a valid email address", [MessageKey.Texts.EqualTo] = "Must be equal to {value}", [MessageKey.Texts.NotEqualTo] = "Must not be equal to {value}", [MessageKey.Texts.Contains] = "Must contain {value}", [MessageKey.Texts.NotContains] = "Must not contain {value}", [MessageKey.Texts.NotEmpty] = "Must not be empty", [MessageKey.Texts.NotWhiteSpace] = "Must not consist only of whitespace characters", [MessageKey.Texts.SingleLine] = "Must consist of single line", [MessageKey.Texts.ExactLength] = "Must be exact {length} characters in length", [MessageKey.Texts.MaxLength] = "Must be at most {max} characters in length", [MessageKey.Texts.MinLength] = "Must be at least {min} characters in length", [MessageKey.Texts.LengthBetween] = "Must be between {min} and {max} characters in length", [MessageKey.Texts.Matches] = "Must match RegEx pattern {pattern}", [MessageKey.Texts.StartsWith] = "Must start with {value}", [MessageKey.Texts.EndsWith] = "Must end with {value}", [MessageKey.Times.EqualTo] = "Must be equal to {value}", [MessageKey.Times.NotEqualTo] = "Must not be equal to {value}", [MessageKey.Times.After] = "Must be after than {min}", [MessageKey.Times.AfterOrEqualTo] = "Must be after than or equal to {min}", [MessageKey.Times.Before] = "Must be before than {max}", [MessageKey.Times.BeforeOrEqualTo] = "Must be before than or equal to {max}", [MessageKey.Times.Between] = "Must be between {min} and {max} (exclusive)", [MessageKey.Times.BetweenOrEqualTo] = "Must be between {min} and {max} (inclusive)", [MessageKey.TimeSpanType.EqualTo] = "Must be equal to {value}", [MessageKey.TimeSpanType.NotEqualTo] = "Must not be equal to {value}", [MessageKey.TimeSpanType.GreaterThan] = "Must be greater than {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Must be greater than or equal to {min}", [MessageKey.TimeSpanType.LessThan] = "Must be less than {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Must be less than or equal to {max}", [MessageKey.TimeSpanType.Between] = "Must be between {min} and {max} (exclusive)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Must be between {min} and {max} (inclusive)", [MessageKey.TimeSpanType.NonZero] = "Must not be zero", [MessageKey.TimeSpanType.Positive] = "Must be positive", [MessageKey.TimeSpanType.NonPositive] = "Must not be positive", [MessageKey.TimeSpanType.Negative] = "Must be negative", [MessageKey.TimeSpanType.NonNegative] = "Must not be negative" }; } } ================================================ FILE: src/Validot/Translations/English/EnglishTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds English translation dictionary for the error messages used in the built-in rules. /// This added by default (for Validators created with Validator.Factory.Create). /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithEnglishTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.English), Translation.English); } } } ================================================ FILE: src/Validot/Translations/German/GermanTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary German { get; } = new Dictionary { [MessageKey.Global.Error] = "Fehler", [MessageKey.Global.Required] = "Erforderlich", [MessageKey.Global.Forbidden] = "Verboten", [MessageKey.Global.ReferenceLoop] = "(Referenzschleife)", [MessageKey.BoolType.True] = "Muss wahr sein", [MessageKey.BoolType.False] = "Darf nicht wahr sein", [MessageKey.CharType.EqualToIgnoreCase] = "Muss gleich sein wie {value} (Groß-/Kleinschreibung wird ignoriert)", [MessageKey.CharType.NotEqualToIgnoreCase] = "Darf nicht gleich sein wie {value} (Groß-/Kleinschreibung wird ignoriert)", [MessageKey.GuidType.EqualTo] = "Muss gleich sein wie {value}", [MessageKey.GuidType.NotEqualTo] = "Darf nicht gleich sein wie {value}", [MessageKey.GuidType.NotEmpty] = "Darf nicht nur nullen sein", [MessageKey.Collections.EmptyCollection] = "Muss leer sein", [MessageKey.Collections.NotEmptyCollection] = "Darf nicht leer sein", [MessageKey.Collections.ExactCollectionSize] = "Muss genau {size} Elemente haben", [MessageKey.Collections.MinCollectionSize] = "Muss mindestens {min} Elemente haben", [MessageKey.Collections.MaxCollectionSize] = "Muss maximal {max} Elemente haben", [MessageKey.Collections.CollectionSizeBetween] = "Muss zwischen {min} und {max} Elemente haben", [MessageKey.Numbers.EqualTo] = "Muss gleich sein wie {value}", [MessageKey.Numbers.NotEqualTo] = "Darf nicht gleich sein wie {value}", [MessageKey.Numbers.GreaterThan] = "Muss mehr sein als {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Muss größer oder gleich sein als {min}", [MessageKey.Numbers.LessThan] = "Muss kleiner sein als {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Muss kleiner oder gleich sein als {max}", [MessageKey.Numbers.Between] = "Muss zwischen {min} und {max} liegen (exklusiv)", [MessageKey.Numbers.BetweenOrEqualTo] = "Muss zwischen {min} und {max} liegen (exklusiv)", [MessageKey.Numbers.NonZero] = "Darf nicht Null sein", [MessageKey.Numbers.Positive] = "Muss positiv sein", [MessageKey.Numbers.NonPositive] = "Darf nicht positiv sein", [MessageKey.Numbers.Negative] = "Muss negativ sein", [MessageKey.Numbers.NonNegative] = "Darf nicht negativ sein", [MessageKey.Numbers.NonNaN] = "Darf nicht NaN sein", [MessageKey.Texts.Email] = "Muss eine gültige E-Mail-Adresse sein", [MessageKey.Texts.EqualTo] = "Muss gleich sein wie {value}", [MessageKey.Texts.NotEqualTo] = "Darf nicht gleich sein als {value}", [MessageKey.Texts.Contains] = "Muss {value} enthalten", [MessageKey.Texts.NotContains] = "Darf {value} nicht enthalten", [MessageKey.Texts.NotEmpty] = "Darf nicht leer sein", [MessageKey.Texts.NotWhiteSpace] = "Darf nicht nur aus Leerzeichen bestehen", [MessageKey.Texts.SingleLine] = "Muss aus einer einzigen Zeile bestehen", [MessageKey.Texts.ExactLength] = "Muss genau {length} Zeichen lang sein", [MessageKey.Texts.MaxLength] = "Muss maximal {max} Zeichen lang sein", [MessageKey.Texts.MinLength] = "Muss mindestens {min} Zeichen lang sein", [MessageKey.Texts.LengthBetween] = "Muss zwischen {min} und {max} Zeichen lang sein", [MessageKey.Texts.Matches] = "Muss dem RegEx-Muster {pattern} entsprechen", [MessageKey.Texts.StartsWith] = "Muss mit {value} beginnen", [MessageKey.Texts.EndsWith] = "Muss mit {value} enden", [MessageKey.Times.EqualTo] = "Muss zum gleichen Zeitpunkt sein sein wie {value}", [MessageKey.Times.NotEqualTo] = "Darf um gleichen Zeitpunkt sein sein wie {value}", [MessageKey.Times.After] = "Muss nach dem {min} liegen", [MessageKey.Times.AfterOrEqualTo] = "Muss danach oder zum gleichen Zeitpunkt sein wie {min}", [MessageKey.Times.Before] = "Muss vor dem {max} liegen", [MessageKey.Times.BeforeOrEqualTo] = "Muss davor oder zum gleichen Zeitpunkt sein wie {max}", [MessageKey.Times.Between] = "Muss zwischen dem {min} und dem {max} liegen (exklusiv)", [MessageKey.Times.BetweenOrEqualTo] = "Muss zwischen dem {min} und dem {max} liegen (einschließlich)", [MessageKey.TimeSpanType.EqualTo] = "Muss gleich sein wie {value}", [MessageKey.TimeSpanType.NotEqualTo] = "Darf nicht gleich sein wie {value}", [MessageKey.TimeSpanType.GreaterThan] = "Muss größer sein als {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Muss größer als oder gleich sein wie {min}", [MessageKey.TimeSpanType.LessThan] = "Muss kleiner sein als {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Muss kleiner als oder gleich sein wie {max}", [MessageKey.TimeSpanType.Between] = "Muss zwischen {min} und {max} liegen (exklusiv)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Muss zwischen {min} und {max} liegen (einschließlich)", [MessageKey.TimeSpanType.NonZero] = "Darf nicht Null sein", [MessageKey.TimeSpanType.Positive] = "Muss positiv sein", [MessageKey.TimeSpanType.NonPositive] = "Darf nicht positiv sein", [MessageKey.TimeSpanType.Negative] = "Muss negativ sein", [MessageKey.TimeSpanType.NonNegative] = "Darf nicht negativ sein" }; } } ================================================ FILE: src/Validot/Translations/German/GermanTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds German translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithGermanTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.German), Translation.German); } } } ================================================ FILE: src/Validot/Translations/ITranslationCompiler.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public interface ITranslationCompiler { IReadOnlyDictionary> Translations { get; } void Add(string name, string messageKey, string translation); } } ================================================ FILE: src/Validot/Translations/MessageKey.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; using System.Reflection; public static class MessageKey { static MessageKey() { var keys = new List(); var globalType = typeof(MessageKey); var innerTypes = globalType.GetNestedTypes(BindingFlags.Public | BindingFlags.Static); foreach (var innerType in innerTypes) { var properties = innerType.GetProperties(BindingFlags.Public | BindingFlags.Static); foreach (var property in properties) { var key = $"{innerType.Name}.{property.Name}"; keys.Add(key); property.SetValue(null, key); } } All = keys.ToArray(); } public static IReadOnlyCollection All { get; } private static void Touch() { } public static class GuidType { static GuidType() { Touch(); } public static string EqualTo { get; private set; } public static string NotEqualTo { get; private set; } public static string NotEmpty { get; private set; } } public static class BoolType { static BoolType() { Touch(); } public static string True { get; private set; } public static string False { get; private set; } } public static class Collections { static Collections() { Touch(); } public static string EmptyCollection { get; private set; } public static string NotEmptyCollection { get; private set; } public static string ExactCollectionSize { get; private set; } public static string MaxCollectionSize { get; private set; } public static string MinCollectionSize { get; private set; } public static string CollectionSizeBetween { get; private set; } } public static class Numbers { static Numbers() { Touch(); } public static string EqualTo { get; private set; } public static string NotEqualTo { get; private set; } public static string GreaterThan { get; private set; } public static string GreaterThanOrEqualTo { get; private set; } public static string LessThan { get; private set; } public static string LessThanOrEqualTo { get; private set; } public static string Between { get; private set; } public static string BetweenOrEqualTo { get; private set; } public static string NonZero { get; private set; } public static string Positive { get; private set; } public static string NonPositive { get; private set; } public static string Negative { get; private set; } public static string NonNegative { get; private set; } public static string NonNaN { get; private set; } } public static class Texts { static Texts() { Touch(); } public static string Email { get; private set; } public static string EqualTo { get; private set; } public static string NotEqualTo { get; private set; } public static string Contains { get; private set; } public static string NotContains { get; private set; } public static string NotEmpty { get; private set; } public static string NotWhiteSpace { get; private set; } public static string SingleLine { get; private set; } public static string ExactLength { get; private set; } public static string MaxLength { get; private set; } public static string MinLength { get; private set; } public static string LengthBetween { get; private set; } public static string Matches { get; private set; } public static string StartsWith { get; private set; } public static string EndsWith { get; private set; } } public static class CharType { static CharType() { Touch(); } public static string EqualToIgnoreCase { get; private set; } public static string NotEqualToIgnoreCase { get; private set; } } public static class Times { static Times() { Touch(); } public static string EqualTo { get; private set; } public static string NotEqualTo { get; private set; } public static string After { get; private set; } public static string AfterOrEqualTo { get; private set; } public static string Before { get; private set; } public static string BeforeOrEqualTo { get; private set; } public static string Between { get; private set; } public static string BetweenOrEqualTo { get; private set; } } public static class TimeSpanType { static TimeSpanType() { Touch(); } public static string EqualTo { get; private set; } public static string NotEqualTo { get; private set; } public static string GreaterThan { get; private set; } public static string GreaterThanOrEqualTo { get; private set; } public static string LessThan { get; private set; } public static string LessThanOrEqualTo { get; private set; } public static string Between { get; private set; } public static string BetweenOrEqualTo { get; private set; } public static string NonZero { get; private set; } public static string Positive { get; private set; } public static string NonPositive { get; private set; } public static string Negative { get; private set; } public static string NonNegative { get; private set; } } public static class Global { static Global() { Touch(); } public static string Forbidden { get; private set; } public static string Required { get; private set; } public static string Error { get; private set; } public static string ReferenceLoop { get; private set; } } } } ================================================ FILE: src/Validot/Translations/Polish/PolishTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary Polish { get; } = new Dictionary { [MessageKey.Global.Error] = "Błąd", [MessageKey.Global.Required] = "Wymagane", [MessageKey.Global.Forbidden] = "Zakazane", [MessageKey.Global.ReferenceLoop] = "(pętla referencji)", [MessageKey.BoolType.True] = "Musi być prawdą", [MessageKey.BoolType.False] = "Musi byc fałszem", [MessageKey.CharType.EqualToIgnoreCase] = "Musi być równe {value} (ignorując wielkość liter)", [MessageKey.CharType.NotEqualToIgnoreCase] = "Musi nie być równe {value} (ignorując wielkość liter)", [MessageKey.GuidType.EqualTo] = "Musi być równe {value}", [MessageKey.GuidType.NotEqualTo] = "Musi nie być równe {value}", [MessageKey.GuidType.NotEmpty] = "Musi nie być samymi zerami", [MessageKey.Collections.EmptyCollection] = "Musi być puste", [MessageKey.Collections.NotEmptyCollection] = "Musi nie być puste", [MessageKey.Collections.ExactCollectionSize] = "Musi zawierać dokładnie {size} elementów", [MessageKey.Collections.MinCollectionSize] = "Musi zawierać minimalnie {min} elementów", [MessageKey.Collections.MaxCollectionSize] = "Musi zawierać maksymalnie {max} elementów", [MessageKey.Collections.CollectionSizeBetween] = "Musi zawierać pomiędzy {min} a {max} elementów", [MessageKey.Numbers.EqualTo] = "Musi być równe {value}", [MessageKey.Numbers.NotEqualTo] = "Musi nie być równe {value}", [MessageKey.Numbers.GreaterThan] = "Musi być większe od {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Musi być większe od lub równe {min}", [MessageKey.Numbers.LessThan] = "Musi być mniejsze od {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Musi być mniejsze od lub równe {max}", [MessageKey.Numbers.Between] = "Musi być pomiędzy {min} a {max} (rozłącznie)", [MessageKey.Numbers.BetweenOrEqualTo] = "Musi być pomiędzy {min} a {max} (włącznie)", [MessageKey.Numbers.NonZero] = "Musi nie być zerem", [MessageKey.Numbers.Positive] = "Musi być pozytywne", [MessageKey.Numbers.NonPositive] = "Musi nie być pozytywne", [MessageKey.Numbers.Negative] = "Musi być negatywne", [MessageKey.Numbers.NonNegative] = "Musi nie być negatywne", [MessageKey.Numbers.NonNaN] = "Musi nie być NaN", [MessageKey.Texts.Email] = "Musi być poprawnym adresem email", [MessageKey.Texts.EqualTo] = "Musi być równe {value}", [MessageKey.Texts.NotEqualTo] = "Musi nie być równe {value}", [MessageKey.Texts.Contains] = "Musi zawierać {value}", [MessageKey.Texts.NotContains] = "Musi nie zawierać {value}", [MessageKey.Texts.NotEmpty] = "Musi nie być puste", [MessageKey.Texts.NotWhiteSpace] = "Musi nie zawierać wyłącznie białych znaków", [MessageKey.Texts.SingleLine] = "Musi zawierać jedynie pojedynczą linię", [MessageKey.Texts.ExactLength] = "Musi być długości dokładnie {length} znaków", [MessageKey.Texts.MaxLength] = "Musi być długości maksymalnie {max} znaków", [MessageKey.Texts.MinLength] = "Musi być długości minimalnie {min} znaków", [MessageKey.Texts.LengthBetween] = "Musi być długości pomiędzy {min} a {max} znaków", [MessageKey.Texts.Matches] = "Musi spełniać wzorzec RegEx {pattern}", [MessageKey.Texts.StartsWith] = "Musi zaczynać się od {value}", [MessageKey.Texts.EndsWith] = "Musi kończyć się od {value}", [MessageKey.Times.EqualTo] = "Musi być równe {value}", [MessageKey.Times.NotEqualTo] = "Musi nie być równe {value}", [MessageKey.Times.After] = "Musi być później niż {min}", [MessageKey.Times.AfterOrEqualTo] = "Musi być później niż lub równe {min}", [MessageKey.Times.Before] = "Musi być wcześniej niż {max}", [MessageKey.Times.BeforeOrEqualTo] = "Musi być wcześniej niż lub równe {max}", [MessageKey.Times.Between] = "Musi być pomiędzy {min} a {max} (rozłącznie)", [MessageKey.Times.BetweenOrEqualTo] = "Musi być pomiędzy {min} a {max} (włącznie)", [MessageKey.TimeSpanType.EqualTo] = "Musi być równe {value}", [MessageKey.TimeSpanType.NotEqualTo] = "Musi nie być równe {value}", [MessageKey.TimeSpanType.GreaterThan] = "Musi być większe od {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Musi być większe od lub równe {min}", [MessageKey.TimeSpanType.LessThan] = "Musi być mniejsze od {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Musi być mniejsze od lub równe {max}", [MessageKey.TimeSpanType.Between] = "Musi być pomiędzy {min} a {max} (rozłącznie)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Musi być pomiędzy {min} a {max} (włącznie)", [MessageKey.TimeSpanType.NonZero] = "Musi nie być zerem", [MessageKey.TimeSpanType.Positive] = "Musi być pozytywne", [MessageKey.TimeSpanType.NonPositive] = "Musi nie być pozytywne", [MessageKey.TimeSpanType.Negative] = "Musi być negatywne", [MessageKey.TimeSpanType.NonNegative] = "Musi nie być negatywne" }; } } ================================================ FILE: src/Validot/Translations/Polish/PolishTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds Polish translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithPolishTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.Polish), Translation.Polish); } } } ================================================ FILE: src/Validot/Translations/Portuguese/PortugueseTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary Portuguese { get; } = new Dictionary { [MessageKey.Global.Error] = "Erro", [MessageKey.Global.Required] = "Obrigatório", [MessageKey.Global.Forbidden] = "Proibido", [MessageKey.Global.ReferenceLoop] = "(ciclo de referência)", [MessageKey.BoolType.True] = "Deve ser verdadeiro", [MessageKey.BoolType.False] = "Deve ser falso", [MessageKey.CharType.EqualToIgnoreCase] = "Deve ser igual a {value} (ignorando maiúsculas e minúsculas)", [MessageKey.CharType.NotEqualToIgnoreCase] = "Não deve ser igual a {value} (ignorando maiúsculas e minúsculas)", [MessageKey.GuidType.EqualTo] = "Deve ser igual a {value}", [MessageKey.GuidType.NotEqualTo] = "Não deve ser igual a {value}", [MessageKey.GuidType.NotEmpty] = "Não deve ser só zeros", [MessageKey.Collections.EmptyCollection] = "Deve estar vazio", [MessageKey.Collections.NotEmptyCollection] = "Não deve estar vazio", [MessageKey.Collections.ExactCollectionSize] = "Deve conter exatamente {size} elementos", [MessageKey.Collections.MinCollectionSize] = "Deve conter no mínimo {min} elementos", [MessageKey.Collections.MaxCollectionSize] = "Deve conter no máximo {max} elementos", [MessageKey.Collections.CollectionSizeBetween] = "Deve conter entre {min} e {max} elementos", [MessageKey.Numbers.EqualTo] = "Deve ser igual a {value}", [MessageKey.Numbers.NotEqualTo] = "Não deve ser igual a {value}", [MessageKey.Numbers.GreaterThan] = "Deve ser maior que {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Deve ser maior ou igual a {min}", [MessageKey.Numbers.LessThan] = "Deve ser menor que {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Deve ser menor ou igual a {max}", [MessageKey.Numbers.Between] = "Deve estar entre {min} e {max} (exclusivo)", [MessageKey.Numbers.BetweenOrEqualTo] = "Deve estar entre {min} e {max} (inclusive)", [MessageKey.Numbers.NonZero] = "Não deve ser zero", [MessageKey.Numbers.Positive] = "Deve ser positivo", [MessageKey.Numbers.NonPositive] = "Não deve ser positivo", [MessageKey.Numbers.Negative] = "Deve ser negativo", [MessageKey.Numbers.NonNegative] = "Não deve ser negativo", [MessageKey.Numbers.NonNaN] = "Não deve ser NaN", [MessageKey.Texts.Email] = "Deve ser um endereço de email válido", [MessageKey.Texts.EqualTo] = "Deve ser igual a {value}", [MessageKey.Texts.NotEqualTo] = "Não deve ser igual a {value}", [MessageKey.Texts.Contains] = "Deve conter {value}", [MessageKey.Texts.NotContains] = "Não deve conter {value}", [MessageKey.Texts.NotEmpty] = "Não deve estar vazio", [MessageKey.Texts.NotWhiteSpace] = "Não deve consistir apenas em caracteres de espaço em branco", [MessageKey.Texts.SingleLine] = "Deve consistir em uma única linha", [MessageKey.Texts.ExactLength] = "Deve ter {length} caracteres", [MessageKey.Texts.MaxLength] = "Deve ter no máximo {max} caracteres", [MessageKey.Texts.MinLength] = "Deve ter no mínimo {min} caracteres", [MessageKey.Texts.LengthBetween] = "Deve ter entre {min} e {max} caracteres", [MessageKey.Texts.Matches] = "Deve corresponder ao padrão de expressão regular {pattern}", [MessageKey.Texts.StartsWith] = "Deve começar com {value}", [MessageKey.Texts.EndsWith] = "Deve terminar com {value}", [MessageKey.Times.EqualTo] = "Deve ser igual a {value}", [MessageKey.Times.NotEqualTo] = "Não deve ser igual a {value}", [MessageKey.Times.After] = "Deve ser posterior a {min}", [MessageKey.Times.AfterOrEqualTo] = "Deve ser igual ou posterior a {min}", [MessageKey.Times.Before] = "Deve ser anterior a {max}", [MessageKey.Times.BeforeOrEqualTo] = "Deve ser igual ou anterior a {max}", [MessageKey.Times.Between] = "Deve estar entre {min} e {max} (exclusivo)", [MessageKey.Times.BetweenOrEqualTo] = "Deve estar entre {min} e {max} (inclusive)", [MessageKey.TimeSpanType.EqualTo] = "Deve ser igual a {value}", [MessageKey.TimeSpanType.NotEqualTo] = "Não deve ser igual a {value}", [MessageKey.TimeSpanType.GreaterThan] = "Deve ser maior que {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Deve ser maior ou igual que {min}", [MessageKey.TimeSpanType.LessThan] = "Deve ser menor que {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Deve ser menor ou igual que {max}", [MessageKey.TimeSpanType.Between] = "Deve estar entre {min} e {max} (exclusivo)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Deve estar entre {min} e {max} (inclusivo)", [MessageKey.TimeSpanType.NonZero] = "Não deve ser zero", [MessageKey.TimeSpanType.Positive] = "Deve ser positivo", [MessageKey.TimeSpanType.NonPositive] = "Não deve ser positivo", [MessageKey.TimeSpanType.Negative] = "Deve ser negativo", [MessageKey.TimeSpanType.NonNegative] = "Não deve ser negativo" }; } } ================================================ FILE: src/Validot/Translations/Portuguese/PortugueseTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds Portuguese translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithPortugueseTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.Portuguese), Translation.Portuguese); } } } ================================================ FILE: src/Validot/Translations/Russian/RussianTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary Russian { get; } = new Dictionary { [MessageKey.Global.Error] = "Ошибка", [MessageKey.Global.Required] = "Требуется", [MessageKey.Global.Forbidden] = "Запрещается", [MessageKey.Global.ReferenceLoop] = "(ссылочный цикл)", [MessageKey.BoolType.True] = "Должен быть верным", [MessageKey.BoolType.False] = "Должен быть неверным", [MessageKey.CharType.EqualToIgnoreCase] = "Должен быть равен {value} (без учета регистра)", [MessageKey.CharType.NotEqualToIgnoreCase] = "Не должен быть равен {value} (без учета регистра)", [MessageKey.GuidType.EqualTo] = "Должно быть равно {value}", [MessageKey.GuidType.NotEqualTo] = "Не должно быть равно {value}", [MessageKey.GuidType.NotEmpty] = "Не должно полностью состоять из нулей", [MessageKey.Collections.EmptyCollection] = "Должен быть пуст", [MessageKey.Collections.NotEmptyCollection] = "Не должен быть пуст", [MessageKey.Collections.ExactCollectionSize] = "Должен содержать точно {size} элементов", [MessageKey.Collections.MinCollectionSize] = "Должен содержать не менее {min} элементов", [MessageKey.Collections.MaxCollectionSize] = "Должен содержать не более {max} элементов", [MessageKey.Collections.CollectionSizeBetween] = "Должен содержать от {min} до {max} элементов", [MessageKey.Numbers.EqualTo] = "Должно быть равно {value}", [MessageKey.Numbers.NotEqualTo] = "Не должно быть равно {value}", [MessageKey.Numbers.GreaterThan] = "Должно быть больше {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Должно быть больше или равно {min}", [MessageKey.Numbers.LessThan] = "Должно быть меньше {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Должно быть меньше или равно {max}", [MessageKey.Numbers.Between] = "Должно быть от {min} до {max} (исключая)", [MessageKey.Numbers.BetweenOrEqualTo] = "Должно быть от {min} до {max} (включительно)", [MessageKey.Numbers.NonZero] = "Не должно быть нулевым", [MessageKey.Numbers.Positive] = "Должно быть положительным", [MessageKey.Numbers.NonPositive] = "Не должно быть положительным", [MessageKey.Numbers.Negative] = "Должно быть отрицательным", [MessageKey.Numbers.NonNegative] = "Не должно быть отрицательным", [MessageKey.Numbers.NonNaN] = "Не должно быть NaN", [MessageKey.Texts.Email] = "Должен быть корректный адрес электронной почты", [MessageKey.Texts.EqualTo] = "Должен быть равен {value}", [MessageKey.Texts.NotEqualTo] = "Не должен быть равен {value}", [MessageKey.Texts.Contains] = "Должен содержать {value}", [MessageKey.Texts.NotContains] = "Не должен содержать {value}", [MessageKey.Texts.NotEmpty] = "Не должен быть пуст", [MessageKey.Texts.NotWhiteSpace] = "Не должен состоять только из пробелов", [MessageKey.Texts.SingleLine] = "Должен состоять из одной строки", [MessageKey.Texts.ExactLength] = "Должен быть точно {length} символов в длину", [MessageKey.Texts.MaxLength] = "Должен быть не больше {max} символов в длину", [MessageKey.Texts.MinLength] = "Должен быть не менее {min} символов в длину", [MessageKey.Texts.LengthBetween] = "Должен быть от {min} до {max} символов в длину", [MessageKey.Texts.Matches] = "Должен соответствовать шаблону регулярного выражения {pattern}", [MessageKey.Texts.StartsWith] = "Должен начинаться с {value}", [MessageKey.Texts.EndsWith] = "Должен заканчиваться на {value}", [MessageKey.Times.EqualTo] = "Должно быть равно {value}", [MessageKey.Times.NotEqualTo] = "Не должно быть равно {value}", [MessageKey.Times.After] = "Должно быть позже, чем {min}", [MessageKey.Times.AfterOrEqualTo] = "Должно быть позже или равно {min}", [MessageKey.Times.Before] = "Должно быть раньше, чем {max}", [MessageKey.Times.BeforeOrEqualTo] = "Должно быть раньше или равно {max}", [MessageKey.Times.Between] = "Должно быть от {min} до {max} (исключая)", [MessageKey.Times.BetweenOrEqualTo] = "Должно быть от {min} до {max} (включительно)", [MessageKey.TimeSpanType.EqualTo] = "Должно быть равно {value}", [MessageKey.TimeSpanType.NotEqualTo] = "Должно быь не равно {value}", [MessageKey.TimeSpanType.GreaterThan] = "Должно быть больше, чем {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Должно быть больше или равно {min}", [MessageKey.TimeSpanType.LessThan] = "Должно быть меньше, чем {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Должно быть меньше или равно {max}", [MessageKey.TimeSpanType.Between] = "Должно быть от {min} до {max} (исключая)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Должно быть от {min} до {max} (включительно)", [MessageKey.TimeSpanType.NonZero] = "Не должно быть нулевым", [MessageKey.TimeSpanType.Positive] = "Должно быть положительным", [MessageKey.TimeSpanType.NonPositive] = "Не должно быть положительным", [MessageKey.TimeSpanType.Negative] = "Должно быть отрицательным", [MessageKey.TimeSpanType.NonNegative] = "Не должно быть отрицательным" }; } } ================================================ FILE: src/Validot/Translations/Russian/RussianTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds Russian translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithRussianTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.Russian), Translation.Russian); } } } ================================================ FILE: src/Validot/Translations/Spanish/SpanishTranslation.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary Spanish { get; } = new Dictionary { [MessageKey.Global.Error] = "Error", [MessageKey.Global.Required] = "Requerido", [MessageKey.Global.Forbidden] = "Prohibido", [MessageKey.Global.ReferenceLoop] = "(bucle de referencia)", [MessageKey.BoolType.True] = "Debe ser verdadero", [MessageKey.BoolType.False] = "Debe ser falso", [MessageKey.CharType.EqualToIgnoreCase] = "Debe ser igual a {value} (ignorando el caso)", [MessageKey.CharType.NotEqualToIgnoreCase] = "No debe ser igual a {value} (ignorando el caso)", [MessageKey.GuidType.EqualTo] = "Debe ser igual {value}", [MessageKey.GuidType.NotEqualTo] = "No debe ser igual a {value}", [MessageKey.GuidType.NotEmpty] = "No deben ser cero", [MessageKey.Collections.EmptyCollection] = "Debe estar vacío", [MessageKey.Collections.NotEmptyCollection] = "No debe estar vacío", [MessageKey.Collections.ExactCollectionSize] = "Debe contener {size} elementos", [MessageKey.Collections.MinCollectionSize] = "Debe contener como mínimo {min} elementos", [MessageKey.Collections.MaxCollectionSize] = "Debe contener como máximo {max} elementos", [MessageKey.Collections.CollectionSizeBetween] = "Debe contener entre {min} y {max} elementos", [MessageKey.Numbers.EqualTo] = "Debe ser igual a {value}", [MessageKey.Numbers.NotEqualTo] = "No debe ser igual {value}", [MessageKey.Numbers.GreaterThan] = "Debe ser mayor que {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Debe ser mayor o igual que {min}", [MessageKey.Numbers.LessThan] = "Debe ser menor que {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Debe ser menor o igual que {max}", [MessageKey.Numbers.Between] = "Debe estar entre {min} y {max} (exclusivo)", [MessageKey.Numbers.BetweenOrEqualTo] = "Debe estar entre {min} y {max} (inclusive)", [MessageKey.Numbers.NonZero] = "No debe ser cero", [MessageKey.Numbers.Positive] = "Debe ser positivo", [MessageKey.Numbers.NonPositive] = "No debe ser positivo", [MessageKey.Numbers.Negative] = "Debe ser negativo", [MessageKey.Numbers.NonNegative] = "No debe ser negativo", [MessageKey.Numbers.NonNaN] = "No debe ser NaN", [MessageKey.Texts.Email] = "Debe ser un correo electrónico válido", [MessageKey.Texts.EqualTo] = "Debe ser igual a {value}", [MessageKey.Texts.NotEqualTo] = "No debe ser igual a {value}", [MessageKey.Texts.Contains] = "Debe contener {value}", [MessageKey.Texts.NotContains] = "No debe contener {value}", [MessageKey.Texts.NotEmpty] = "No debe estar vacío", [MessageKey.Texts.NotWhiteSpace] = "No debe contener espacios", [MessageKey.Texts.SingleLine] = "Debe constar de una sola línea", [MessageKey.Texts.ExactLength] = "Debe tener {length} caracteres", [MessageKey.Texts.MaxLength] = "Debe tener como máximo {max} caracteres", [MessageKey.Texts.MinLength] = "Debe tener al menos {min} caracteres", [MessageKey.Texts.LengthBetween] = "Debe tener entre {min} y {max} caracteres", [MessageKey.Texts.Matches] = "Debe coincidir con el patrón de expresión regular {pattern}", [MessageKey.Texts.StartsWith] = "Debe comenzar con {value}", [MessageKey.Texts.EndsWith] = "Debe terminar con {value}", [MessageKey.Times.EqualTo] = "Debe ser igual a {value}", [MessageKey.Times.NotEqualTo] = "No debe ser igual a {value}", [MessageKey.Times.After] = "Debe ser posterior a {min}", [MessageKey.Times.AfterOrEqualTo] = "Debe ser igual o posterior a {min}", [MessageKey.Times.Before] = "Debe ser anterior a {max}", [MessageKey.Times.BeforeOrEqualTo] = "Debe ser igual o anterior a {max}", [MessageKey.Times.Between] = "Debe estar entre {min} y {max} (exclusivo)", [MessageKey.Times.BetweenOrEqualTo] = "Debe estar entre {min} y {max} (inclusive)", [MessageKey.TimeSpanType.EqualTo] = "Debe ser igual a {value}", [MessageKey.TimeSpanType.NotEqualTo] = "No debe ser igual a {value}", [MessageKey.TimeSpanType.GreaterThan] = "Debe ser mayor que {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Debe ser mayor o igual que {min}", [MessageKey.TimeSpanType.LessThan] = "Debe ser menor que {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Debe ser menor o igual que {max}", [MessageKey.TimeSpanType.Between] = "Debe estar entre {min} y {max} (exclusivo)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Debe estar entre {min} y {max} (inclusivo)", [MessageKey.TimeSpanType.NonZero] = "No debe ser cero", [MessageKey.TimeSpanType.Positive] = "Debe ser positivo", [MessageKey.TimeSpanType.NonPositive] = "No debe ser positivo", [MessageKey.TimeSpanType.Negative] = "Debe ser negativo", [MessageKey.TimeSpanType.NonNegative] = "No debe ser negativo" }; } } ================================================ FILE: src/Validot/Translations/Spanish/SpanishTranslationsExtensions.cs ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds Spanish translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings WithSpanishTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation.Spanish), Translation.Spanish); } } } ================================================ FILE: src/Validot/Translations/TranslationCompiler.cs ================================================ namespace Validot.Translations { using System.Collections.Generic; using System.Linq; internal class TranslationCompiler : ITranslationCompiler { private readonly Dictionary> _translations = new Dictionary>(); private IReadOnlyDictionary> _cachedResults; private bool _changed = true; public IReadOnlyDictionary> Translations { get { if (_changed) { _cachedResults = _translations.ToDictionary(p => p.Key, p => (IReadOnlyDictionary)p.Value); _changed = false; } return _cachedResults; } } public void Add(string name, string messageKey, string translation) { ThrowHelper.NullArgument(name, nameof(name)); ThrowHelper.NullArgument(messageKey, nameof(messageKey)); ThrowHelper.NullArgument(translation, nameof(translation)); _changed = true; if (!_translations.ContainsKey(name)) { _translations.Add(name, new Dictionary { [messageKey] = translation }); return; } _translations[name][messageKey] = translation; } } } ================================================ FILE: src/Validot/Translations/_Template/_TemplateTranslation.cs.txt ================================================ namespace Validot.Translations { using System.Collections.Generic; public static partial class Translation { public static IReadOnlyDictionary _Template { get; } = new Dictionary { [MessageKey.Global.Error] = "Error", [MessageKey.Global.Required] = "Required", [MessageKey.Global.Forbidden] = "Forbidden", [MessageKey.Global.ReferenceLoop] = "(reference loop)", [MessageKey.BoolType.True] = "Must be true", [MessageKey.BoolType.False] = "Must be false", [MessageKey.CharType.EqualToIgnoreCase] = "Must be equal to {value} (ignoring case)", [MessageKey.CharType.NotEqualToIgnoreCase] = "Must not be equal to {value} (ignoring case)", [MessageKey.GuidType.EqualTo] = "Must be equal to {value}", [MessageKey.GuidType.NotEqualTo] = "Must not be equal to {value}", [MessageKey.GuidType.NotEmpty] = "Must not be all zeros", [MessageKey.Collections.EmptyCollection] = "Must be empty", [MessageKey.Collections.NotEmptyCollection] = "Must not be empty", [MessageKey.Collections.ExactCollectionSize] = "Must contain exactly {size} items", [MessageKey.Collections.MinCollectionSize] = "Must contain at least {min} items", [MessageKey.Collections.MaxCollectionSize] = "Must contain at most {max} items", [MessageKey.Collections.CollectionSizeBetween] = "Must contain between {min} and {max} items", [MessageKey.Numbers.EqualTo] = "Must be equal to {value}", [MessageKey.Numbers.NotEqualTo] = "Must not be equal to {value}", [MessageKey.Numbers.GreaterThan] = "Must be greater than {min}", [MessageKey.Numbers.GreaterThanOrEqualTo] = "Must be greater than or equal to {min}", [MessageKey.Numbers.LessThan] = "Must be less than {max}", [MessageKey.Numbers.LessThanOrEqualTo] = "Must be less than or equal to {max}", [MessageKey.Numbers.Between] = "Must be between {min} and {max} (exclusive)", [MessageKey.Numbers.BetweenOrEqualTo] = "Must be between {min} and {max} (inclusive)", [MessageKey.Numbers.NonZero] = "Must not be zero", [MessageKey.Numbers.Positive] = "Must be positive", [MessageKey.Numbers.NonPositive] = "Must not be positive", [MessageKey.Numbers.Negative] = "Must be negative", [MessageKey.Numbers.NonNegative] = "Must not be negative", [MessageKey.Numbers.NonNaN] = "Must not be NaN", [MessageKey.Texts.Email] = "Must be a valid email address", [MessageKey.Texts.EqualTo] = "Must be equal to {value}", [MessageKey.Texts.NotEqualTo] = "Must not be equal to {value}", [MessageKey.Texts.Contains] = "Must contain {value}", [MessageKey.Texts.NotContains] = "Must not contain {value}", [MessageKey.Texts.NotEmpty] = "Must not be empty", [MessageKey.Texts.NotWhiteSpace] = "Must not consist only of whitespace characters", [MessageKey.Texts.SingleLine] = "Must consist of single line", [MessageKey.Texts.ExactLength] = "Must be exact {length} characters in length", [MessageKey.Texts.MaxLength] = "Must be at most {max} characters in length", [MessageKey.Texts.MinLength] = "Must be at least {min} characters in length", [MessageKey.Texts.LengthBetween] = "Must be between {min} and {max} characters in length", [MessageKey.Texts.Matches] = "Must match RegEx pattern {pattern}", [MessageKey.Texts.StartsWith] = "Must start with {value}", [MessageKey.Texts.EndsWith] = "Must end with {value}", [MessageKey.Times.EqualTo] = "Must be equal to {value}", [MessageKey.Times.NotEqualTo] = "Must not be equal to {value}", [MessageKey.Times.After] = "Must be after than {min}", [MessageKey.Times.AfterOrEqualTo] = "Must be after than or equal to {min}", [MessageKey.Times.Before] = "Must be before than {max}", [MessageKey.Times.BeforeOrEqualTo] = "Must be before than or equal to {max}", [MessageKey.Times.Between] = "Must be between {min} and {max} (exclusive)", [MessageKey.Times.BetweenOrEqualTo] = "Must be between {min} and {max} (inclusive)", [MessageKey.TimeSpanType.EqualTo] = "Must be equal to {value}", [MessageKey.TimeSpanType.NotEqualTo] = "Must not be equal to {value}", [MessageKey.TimeSpanType.GreaterThan] = "Must be greater than {min}", [MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Must be greater than or equal to {min}", [MessageKey.TimeSpanType.LessThan] = "Must be less than {max}", [MessageKey.TimeSpanType.LessThanOrEqualTo] = "Must be less than or equal to {max}", [MessageKey.TimeSpanType.Between] = "Must be between {min} and {max} (exclusive)", [MessageKey.TimeSpanType.BetweenOrEqualTo] = "Must be between {min} and {max} (inclusive)", [MessageKey.TimeSpanType.NonZero] = "Must not be zero", [MessageKey.TimeSpanType.Positive] = "Must be positive", [MessageKey.TimeSpanType.NonPositive] = "Must not be positive", [MessageKey.TimeSpanType.Negative] = "Must be negative", [MessageKey.TimeSpanType.NonNegative] = "Must not be negative" }; } } ================================================ FILE: src/Validot/Translations/_Template/_TemplateTranslationsExtensions.cs.txt ================================================ namespace Validot { using Validot.Settings; using Validot.Translations; public static partial class TranslationsExtensions { /// /// Adds _Template translation dictionary for the error messages used in the built-in rules. /// /// Settings fluent API builder - input. /// Settings fluent API builder - output. public static ValidatorSettings With_TemplateTranslation(this ValidatorSettings @this) { ThrowHelper.NullArgument(@this, nameof(@this)); return @this.WithTranslation(nameof(Translation._Template), Translation._Template); } } } ================================================ FILE: src/Validot/TypeStringifier.cs ================================================ namespace Validot { using System; using System.Linq; internal static class TypeStringifier { // https://stackoverflow.com/a/47477303 public static string GetFriendlyName(this Type type, bool includeNamespace = false) { var typeName = type.Name; if (type.IsGenericType) { var name = type.Name.Substring(0, type.Name.IndexOf('`')); var types = string.Join(",", type.GetGenericArguments().Select(t => t.GetFriendlyName(includeNamespace))); typeName = $"{name}<{types}>"; } return includeNamespace ? $"{type.Namespace}.{typeName}" : typeName; } } } ================================================ FILE: src/Validot/Validation/DiscoveryContext.cs ================================================ namespace Validot.Validation { using System.Collections.Generic; using Validot.Errors; using Validot.Validation.Stacks; internal class DiscoveryContext : IDiscoveryContext, IErrorsHolder { private readonly IDiscoveryContextActions _actions; private readonly List _referenceLoopRoots = new List(); private readonly PathStack _pathsStack = new PathStack(); private readonly Stack _scopesStack = new Stack(); public DiscoveryContext(IDiscoveryContextActions actions, int rootSpecificationScopeId) { _actions = actions; _scopesStack.Push(rootSpecificationScopeId); } public Dictionary> Paths { get; } = new Dictionary>(); public Dictionary> Errors { get; } = new Dictionary>(); public IReadOnlyCollection ReferenceLoopRoots => _referenceLoopRoots; public void AddError(int errorId, bool skipIfDuplicateInPath = false) { if (!Errors.ContainsKey(_pathsStack.Path)) { Errors.Add(_pathsStack.Path, new List()); } if (skipIfDuplicateInPath && Errors[_pathsStack.Path].Contains(errorId)) { return; } Errors[_pathsStack.Path].Add(errorId); } public void EnterScope(int id) { if (_scopesStack.Contains(id)) { var referenceLoopErrorId = _actions.RegisterError(new ReferenceLoopError(typeof(T))); AddError(referenceLoopErrorId); if (!_referenceLoopRoots.Contains(_pathsStack.Path)) { _referenceLoopRoots.Add(_pathsStack.Path); } return; } _scopesStack.Push(id); var scope = _actions.GetDiscoverableSpecificationScope(id); scope.Discover(this); } public void LeavePath() { _pathsStack.Pop(); } public void EnterCollectionItemPath() { var path = PathHelper.ResolvePath(_pathsStack.Path, PathHelper.CollectionIndexPrefixString); _pathsStack.PushWithDiscoveryIndex(path); } public void EnterPath(string path) { path = path ?? string.Empty; var resolvedPath = PathHelper.ResolvePath(_pathsStack.Path, path); if (!Paths.ContainsKey(_pathsStack.Path)) { Paths.Add(_pathsStack.Path, new Dictionary()); } Paths[_pathsStack.Path][path] = resolvedPath; _pathsStack.Push(resolvedPath); } } } ================================================ FILE: src/Validot/Validation/ErrorFlag.cs ================================================ namespace Validot.Validation { using System.Collections.Generic; internal class ErrorFlag { private readonly Dictionary _detectionForLevels; private readonly Dictionary _errorsForLevels; public ErrorFlag(int capacity = 0) { _detectionForLevels = new Dictionary(capacity); _errorsForLevels = new Dictionary(capacity); } public bool IsEnabledAtAnyLevel => _errorsForLevels.Count > 0; public bool IsDetectedAtAnyLevel { get; private set; } public void SetEnabled(int level, int errorId) { if (_errorsForLevels.ContainsKey(level)) { return; } _errorsForLevels.Add(level, errorId); _detectionForLevels.Add(level, false); } public void SetDetected(int level) { if (!IsEnabledAtAnyLevel) { return; } foreach (var enabledLevel in _errorsForLevels.Keys) { if (enabledLevel <= level) { _detectionForLevels[enabledLevel] = true; IsDetectedAtAnyLevel = true; } } } public bool LeaveLevelAndTryGetError(int level, out int errorId) { if (_errorsForLevels.TryGetValue(level, out errorId)) { var detected = _detectionForLevels[level]; _ = _errorsForLevels.Remove(level); _ = _detectionForLevels.Remove(level); IsDetectedAtAnyLevel = _detectionForLevels.Count > 0 && _detectionForLevels.ContainsValue(true); if (detected) { return true; } } errorId = -1; return false; } } } ================================================ FILE: src/Validot/Validation/IDiscoveryContext.cs ================================================ namespace Validot.Validation { internal interface IDiscoveryContext { void AddError(int errorId, bool skipIfDuplicateInPath = false); void EnterPath(string name); void LeavePath(); void EnterCollectionItemPath(); void EnterScope(int id); } } ================================================ FILE: src/Validot/Validation/IDiscoveryContextActions.cs ================================================ namespace Validot.Validation { using Validot.Errors; using Validot.Validation.Scopes; internal interface IDiscoveryContextActions { IDiscoverable GetDiscoverableSpecificationScope(int id); int RegisterError(IError error); } } ================================================ FILE: src/Validot/Validation/IErrorsHolder.cs ================================================ namespace Validot.Validation { using System.Collections.Generic; public interface IErrorsHolder { Dictionary> Errors { get; } } } ================================================ FILE: src/Validot/Validation/IValidationContext.cs ================================================ namespace Validot.Validation { using Validot.Validation.Scopes.Builders; internal interface IValidationContext { bool ShouldFallBack { get; } void AddError(int errorId, bool skipIfDuplicateInPath = false); void EnterPath(string path); void LeavePath(); void EnableErrorDetectionMode(ErrorMode errorMode, int errorId); void EnterCollectionItemPath(int i); void EnterScope(int scopeId, T model); } } ================================================ FILE: src/Validot/Validation/IsValidValidationContext.cs ================================================ namespace Validot.Validation { using System; using Validot.Validation.Scheme; using Validot.Validation.Scopes.Builders; using Validot.Validation.Stacks; internal class IsValidValidationContext : IValidationContext { private readonly bool _referencesLoopProtectionEnabled; private readonly ReferencesStack _referencesStack; private readonly IModelScheme _modelScheme; public bool ErrorFound { get; private set; } public bool ShouldFallBack => ErrorFound; public IsValidValidationContext(IModelScheme modelScheme, ReferenceLoopProtectionSettings referenceLoopProtectionSettings) { _modelScheme = modelScheme; ReferenceLoopProtectionSettings = referenceLoopProtectionSettings; if (!(referenceLoopProtectionSettings is null)) { _referencesLoopProtectionEnabled = true; _referencesStack = new ReferencesStack(); if (_modelScheme.RootModelType.IsClass && !(referenceLoopProtectionSettings.RootModelReference is null)) { _ = _referencesStack.TryPush(_modelScheme.RootSpecificationScopeId, string.Empty, referenceLoopProtectionSettings.RootModelReference, out _); } } } public ReferenceLoopProtectionSettings ReferenceLoopProtectionSettings { get; } public void AddError(int errorId, bool skipIfDuplicateInPath = false) { ErrorFound = true; } public void EnterPath(string path) { } public void LeavePath() { } public void EnableErrorDetectionMode(ErrorMode errorMode, int errorId) { } public void EnterCollectionItemPath(int i) { } public void EnterScope(int scopeId, T model) { var useReferenceLoopProtection = typeof(T).IsClass && _referencesLoopProtectionEnabled; if (useReferenceLoopProtection && !_referencesStack.TryPush(scopeId, string.Empty, model, out _)) { FailWithException(scopeId, typeof(T)); return; } var specificationScope = _modelScheme.GetSpecificationScope(scopeId); specificationScope.Validate(model, this); if (useReferenceLoopProtection) { _ = _referencesStack.Pop(scopeId, out _); } } public int? GetLoopProtectionReferencesStackCount() { return _referencesStack?.GetStoredReferencesCount(); } private void FailWithException(int scopeId, Type type) { throw new ReferenceLoopException(scopeId, type); } } } ================================================ FILE: src/Validot/Validation/Scheme/IModelScheme.cs ================================================ namespace Validot.Validation.Scheme { using System; using System.Collections.Generic; using Validot.Errors; using Validot.Validation.Scopes; internal interface IModelScheme { int RootSpecificationScopeId { get; } Type RootModelType { get; } ISpecificationScope GetSpecificationScope(int specificationScopeId); string ResolvePath(string basePath, string relativePath); string GetPathWithIndexes(string path, IReadOnlyCollection indexesStack); } internal interface IModelScheme : IModelScheme { bool IsReferenceLoopPossible { get; } ISpecificationScope RootSpecificationScope { get; } IReadOnlyDictionary> Template { get; } IReadOnlyDictionary ErrorRegistry { get; } } } ================================================ FILE: src/Validot/Validation/Scheme/ModelScheme.cs ================================================ namespace Validot.Validation.Scheme { using System; using System.Collections.Generic; using Validot.Errors; using Validot.Validation.Scopes; internal class ModelScheme : IModelScheme { private readonly IReadOnlyDictionary> _pathMap; private readonly IReadOnlyDictionary _specificationScopes; public ModelScheme(IReadOnlyDictionary specificationScopes, int rootSpecificationScopeId, IReadOnlyDictionary errorRegistry, IReadOnlyDictionary> template, IReadOnlyDictionary> pathMap, bool isReferenceLoopPossible) { ThrowHelper.NullArgument(specificationScopes, nameof(specificationScopes)); ThrowHelper.NullInCollection(specificationScopes.Values, $"{nameof(specificationScopes)}.{nameof(specificationScopes.Values)}"); _specificationScopes = specificationScopes; if (!_specificationScopes.ContainsKey(rootSpecificationScopeId)) { throw new ArgumentException($"{nameof(specificationScopes)} doesn't contain specification scope with id {rootSpecificationScopeId} ({nameof(rootSpecificationScopeId)})"); } if (!(_specificationScopes[rootSpecificationScopeId] is SpecificationScope)) { throw new ArgumentException($"specification scope with id {rootSpecificationScopeId} ({nameof(rootSpecificationScopeId)}) is not of type {typeof(SpecificationScope).FullName}"); } RootSpecificationScope = (SpecificationScope)specificationScopes[rootSpecificationScopeId]; ThrowHelper.NullArgument(errorRegistry, nameof(errorRegistry)); ThrowHelper.NullInCollection(errorRegistry.Values, $"{nameof(errorRegistry)}.{nameof(errorRegistry.Values)}"); ErrorRegistry = errorRegistry; ThrowHelper.NullArgument(template, nameof(template)); ThrowHelper.NullInCollection(template.Values, $"{nameof(template)}.{nameof(template.Values)}"); Template = template; ThrowHelper.NullArgument(pathMap, nameof(pathMap)); ThrowHelper.NullInCollection(pathMap.Values, $"{nameof(pathMap)}.{nameof(pathMap.Values)}"); foreach (var item in pathMap.Values) { foreach (var innerItem in item) { if (innerItem.Value == null) { throw new ArgumentNullException($"Collection `{nameof(pathMap)}` contains null in inner dictionary under key `{innerItem.Key}`"); } } } _pathMap = pathMap; IsReferenceLoopPossible = isReferenceLoopPossible; RootModelType = typeof(T); RootSpecificationScopeId = rootSpecificationScopeId; } public IReadOnlyDictionary ErrorRegistry { get; } public IReadOnlyDictionary> Template { get; } public ISpecificationScope RootSpecificationScope { get; } public bool IsReferenceLoopPossible { get; } public int RootSpecificationScopeId { get; } public Type RootModelType { get; } public string ResolvePath(string basePath, string relativePath) { if (_pathMap.TryGetValue(basePath, out var values)) { if (values.TryGetValue(relativePath, out var value)) { return value; } } return PathHelper.ResolvePath(basePath, relativePath); } public ISpecificationScope GetSpecificationScope(int specificationScopeId) { return (SpecificationScope)_specificationScopes[specificationScopeId]; } public string GetPathWithIndexes(string path, IReadOnlyCollection indexesStack) { return PathHelper.GetWithIndexes(path, indexesStack); } } } ================================================ FILE: src/Validot/Validation/Scheme/ModelSchemeFactory.cs ================================================ namespace Validot.Validation.Scheme { using System.Collections.Generic; using System.Linq; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal static class ModelSchemeFactory { public static IModelScheme Create(Specification specification) { ThrowHelper.NullArgument(specification, nameof(specification)); var scopeBuilderContext = new ScopeBuilderContext(); var rootSpecificationScopeId = scopeBuilderContext.GetOrRegisterSpecificationScope(specification); var discoveryContext = new DiscoveryContext(scopeBuilderContext, rootSpecificationScopeId); var rootSpecificationScope = (SpecificationScope)scopeBuilderContext.Scopes[rootSpecificationScopeId]; rootSpecificationScope.Discover(discoveryContext); return new ModelScheme( scopeBuilderContext.Scopes, rootSpecificationScopeId, scopeBuilderContext.Errors, discoveryContext.Errors.ToDictionary(p => p.Key, p => (IReadOnlyList)p.Value), discoveryContext.Paths.ToDictionary(p => p.Key, p => (IReadOnlyDictionary)p.Value), discoveryContext.ReferenceLoopRoots.Count > 0); } } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/CommandScopeBuilder.cs ================================================ namespace Validot.Validation.Scopes.Builders { using System; using Validot.Specification.Commands; internal class CommandScopeBuilder : ICommandScopeBuilder { private readonly ICommand _command; private readonly Func> _coreBuilder; private ErrorBuilder _errorsBuilder; private string _path; private Predicate _executionCondition; public CommandScopeBuilder(ICommand command, Func> coreBuilder) { ThrowHelper.NullArgument(command, nameof(command)); ThrowHelper.NullArgument(coreBuilder, nameof(coreBuilder)); _command = command; _coreBuilder = coreBuilder; } public ICommandScope Build(IScopeBuilderContext context) { ThrowHelper.NullArgument(context, nameof(context)); var core = _coreBuilder(_command, context); ThrowHelper.NullArgument(core, nameof(core)); if (_errorsBuilder != null) { var error = _errorsBuilder.Build(); core.ErrorMode = _errorsBuilder.Mode; if (!_errorsBuilder.IsEmpty) { core.ErrorId = context.RegisterError(error); } else if (_errorsBuilder.Mode == ErrorMode.Override) { core.ErrorId = context.DefaultErrorId; } } if (_path != null) { core.Path = _path; } if (_executionCondition != null) { core.ExecutionCondition = _executionCondition; } return core; } public bool TryAdd(ICommand command) { ThrowHelper.NullArgument(command, nameof(command)); if (command is WithConditionCommand withConditionCommand) { _executionCondition = withConditionCommand.ExecutionCondition; return true; } if (command is WithPathCommand withPathCommand) { _path = withPathCommand.Path; return true; } if (_errorsBuilder == null) { _errorsBuilder = new ErrorBuilder(); } return _errorsBuilder.TryAdd(command); } } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/ErrorBuilder.cs ================================================ namespace Validot.Validation.Scopes.Builders { using System; using System.Collections.Generic; using System.Linq; using Validot.Errors; using Validot.Errors.Args; using Validot.Specification.Commands; internal class ErrorBuilder { private readonly IReadOnlyList _args; private readonly List _codes = new List(); private readonly List _messages = new List(); public ErrorBuilder() { } public ErrorBuilder(string key, IReadOnlyList args = null) { ThrowHelper.NullArgument(key, nameof(key)); _messages.Add(key); _args = args; } public bool IsEmpty => !_messages.Any() && !_codes.Any(); public ErrorMode Mode { get; private set; } = ErrorMode.Append; public bool TryAdd(ICommand command) { ThrowHelper.NullArgument(command, nameof(command)); if (command is WithMessageCommand withMessageCommand) { ClearError(); _messages.Add(withMessageCommand.Message); } else if (command is WithExtraMessageCommand withExtraMessageCommand) { _messages.Add(withExtraMessageCommand.Message); } else if (command is WithCodeCommand withCodeCommand) { ClearError(); _codes.Add(withCodeCommand.Code); } else if (command is WithExtraCodeCommand withExtraCodeCommand) { _codes.Add(withExtraCodeCommand.Code); } else { return false; } return true; } public IError Build() { return new Error { Args = _args ?? Array.Empty(), Codes = _codes, Messages = _messages }; } private void ClearError() { Mode = ErrorMode.Override; _codes.Clear(); _messages.Clear(); } } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/ErrorMode.cs ================================================ namespace Validot.Validation.Scopes.Builders { internal enum ErrorMode { Append = 0, Override = 1 } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/ICommandScopeBuilder.cs ================================================ namespace Validot.Validation.Scopes.Builders { using Validot.Specification.Commands; internal interface ICommandScopeBuilder { ICommandScope Build(IScopeBuilderContext context); bool TryAdd(ICommand command); } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/IScopeBuilderContext.cs ================================================ namespace Validot.Validation.Scopes.Builders { using Validot.Errors; internal interface IScopeBuilderContext { int DefaultErrorId { get; } int ForbiddenErrorId { get; } int RequiredErrorId { get; } int RegisterError(IError error); int GetOrRegisterSpecificationScope(Specification specification); } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/RuleCommandScopeBuilder.cs ================================================ namespace Validot.Validation.Scopes.Builders { using System; using Validot.Specification.Commands; internal class RuleCommandScopeBuilder : ICommandScopeBuilder { private readonly ErrorBuilder _errorsBuilder = new ErrorBuilder(); private readonly RuleCommand _ruleCommand; private string _path; private Predicate _executionCondition; public RuleCommandScopeBuilder(RuleCommand ruleCommand) { ThrowHelper.NullArgument(ruleCommand, nameof(ruleCommand)); _ruleCommand = ruleCommand; if (_ruleCommand.Message != null) { _errorsBuilder = new ErrorBuilder(_ruleCommand.Message, _ruleCommand.Args); } } public ICommandScope Build(IScopeBuilderContext context) { ThrowHelper.NullArgument(context, nameof(context)); var ruleCommandScope = new RuleCommandScope { IsValid = _ruleCommand.Predicate, Path = _path, ExecutionCondition = _executionCondition, ErrorMode = _errorsBuilder.Mode }; if (_errorsBuilder.IsEmpty) { ruleCommandScope.ErrorId = context.DefaultErrorId; } else { var error = _errorsBuilder.Build(); ruleCommandScope.ErrorId = context.RegisterError(error); } return ruleCommandScope; } public bool TryAdd(ICommand command) { if (command is WithConditionCommand withConditionCommand) { _executionCondition = withConditionCommand.ExecutionCondition; return true; } if (command is WithPathCommand withPathCommand) { _path = withPathCommand.Path; return true; } return _errorsBuilder.TryAdd(command); } } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/ScopeBuilder.cs ================================================ namespace Validot.Validation.Scopes.Builders { using System.Collections.Generic; using Validot.Specification; using Validot.Specification.Commands; using Validot.Translations; internal class ScopeBuilder { public SpecificationScope Build(Specification specification, IScopeBuilderContext context) { ThrowHelper.NullArgument(specification, nameof(specification)); ThrowHelper.NullArgument(context, nameof(context)); var specificationApi = (SpecificationApi)specification(new SpecificationApi()); var commandScopes = new List>(); var presenceInfo = ParsePresenceInfo(specificationApi.Commands, context, out var index); for (var i = index; i < specificationApi.Commands.Count; ++i) { var scopeCommand = (IScopeCommand)specificationApi.Commands[i]; var scopeBuilder = scopeCommand.GetScopeBuilder(); while (++i < specificationApi.Commands.Count) { if (!scopeBuilder.TryAdd(specificationApi.Commands[i])) { break; } } --i; var commandScope = (ICommandScope)scopeBuilder.Build(context); commandScopes.Add(commandScope); } return new SpecificationScope { Presence = presenceInfo.Presence, ForbiddenErrorId = presenceInfo.ForbiddenErrorId, RequiredErrorId = presenceInfo.RequiredErrorId, CommandScopes = commandScopes }; } private static PresenceInfo ParsePresenceInfo(IReadOnlyList commands, IScopeBuilderContext context, out int index) { index = 0; if (commands.Count == 0 || !IsPresenceCommand(commands[0])) { return new PresenceInfo { Presence = Presence.Required, RequiredErrorId = context.RequiredErrorId, ForbiddenErrorId = context.ForbiddenErrorId }; } if (commands[0] is OptionalCommand) { index = 1; return new PresenceInfo { Presence = Presence.Optional, RequiredErrorId = context.RequiredErrorId, ForbiddenErrorId = context.ForbiddenErrorId }; } if (commands[0] is RequiredCommand) { var requiredErrorId = GetErrorId(context.RequiredErrorId, MessageKey.Global.Required, out index); return new PresenceInfo { Presence = Presence.Required, RequiredErrorId = requiredErrorId, ForbiddenErrorId = context.ForbiddenErrorId }; } var forbiddenErrorId = GetErrorId(context.ForbiddenErrorId, MessageKey.Global.Forbidden, out index); return new PresenceInfo { Presence = Presence.Forbidden, RequiredErrorId = context.RequiredErrorId, ForbiddenErrorId = forbiddenErrorId }; int GetErrorId(int defaultErrorId, string baseMessageKey, out int i) { var errorsBuilder = baseMessageKey != null ? new ErrorBuilder(baseMessageKey) : new ErrorBuilder(); var somethingAdded = false; for (i = 1; i < commands.Count; ++i) { if (!errorsBuilder.TryAdd(commands[i])) { break; } somethingAdded = true; } if (!somethingAdded || errorsBuilder.IsEmpty) { return defaultErrorId; } var error = errorsBuilder.Build(); return context.RegisterError(error); } } private static bool IsPresenceCommand(ICommand c) { return c is OptionalCommand || c is RequiredCommand || c is ForbiddenCommand; } private class PresenceInfo { public Presence Presence { get; set; } public int ForbiddenErrorId { get; set; } public int RequiredErrorId { get; set; } } } } ================================================ FILE: src/Validot/Validation/Scopes/Builders/ScopeBuilderContext.cs ================================================ namespace Validot.Validation.Scopes.Builders { using System; using System.Collections.Generic; using Validot.Errors; using Validot.Errors.Args; using Validot.Translations; internal class ScopeBuilderContext : IScopeBuilderContext, IDiscoveryContextActions { private static readonly ScopeBuilder ScopeBuilder = new ScopeBuilder(); private readonly Dictionary _errors = new Dictionary(); private readonly Dictionary _scopes = new Dictionary(); private readonly Dictionary _specifications = new Dictionary(); private readonly Dictionary _types = new Dictionary(); public ScopeBuilderContext() { DefaultErrorId = RegisterError(new Error { Messages = new[] { MessageKey.Global.Error }, Codes = Array.Empty(), Args = Array.Empty() }); ForbiddenErrorId = RegisterError(new Error { Messages = new[] { MessageKey.Global.Forbidden }, Codes = Array.Empty(), Args = Array.Empty() }); RequiredErrorId = RegisterError(new Error { Messages = new[] { MessageKey.Global.Required }, Codes = Array.Empty(), Args = Array.Empty() }); } public IReadOnlyDictionary Errors => _errors; public IReadOnlyDictionary Scopes => _scopes; public IReadOnlyDictionary Types => _types; public int DefaultErrorId { get; } public int ForbiddenErrorId { get; } public int RequiredErrorId { get; } public IDiscoverable GetDiscoverableSpecificationScope(int id) { return (IDiscoverable)_scopes[id]; } public int RegisterError(IError error) { ThrowHelper.NullArgument(error, nameof(error)); var id = _errors.Count; _errors.Add(id, error); return id; } public int GetOrRegisterSpecificationScope(Specification specification) { ThrowHelper.NullArgument(specification, nameof(specification)); foreach (var pair in _specifications) { if (ReferenceEquals(pair.Value, specification)) { return pair.Key; } } var id = _specifications.Count; _specifications.Add(id, specification); _types.Add(id, typeof(T)); var scope = ScopeBuilder.Build(specification, this); _scopes.Add(id, scope); return id; } } } ================================================ FILE: src/Validot/Validation/Scopes/CollectionCommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System.Collections.Generic; internal class CollectionCommandScope : CommandScope where T : IEnumerable { public int ScopeId { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterCollectionItemPath(); context.EnterScope(ScopeId); context.LeavePath(); } protected override void RunValidation(T model, IValidationContext context) { var i = 0; foreach (var item in model) { context.EnterCollectionItemPath(i); context.EnterScope(ScopeId, item); context.LeavePath(); if (context.ShouldFallBack) { break; } ++i; } } } } ================================================ FILE: src/Validot/Validation/Scopes/CommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System; using Validot.Validation.Scopes.Builders; internal abstract class CommandScope : ICommandScope { public Predicate ExecutionCondition { get; set; } public int? ErrorId { get; set; } public ErrorMode ErrorMode { get; set; } = ErrorMode.Append; public string Path { get; set; } public void Discover(IDiscoveryContext context) { context.EnterPath(Path); if (!ErrorId.HasValue || ErrorMode == ErrorMode.Append) { RunDiscovery(context); } if (ErrorId.HasValue) { context.AddError(ErrorId.Value); } context.LeavePath(); } public void Validate(T model, IValidationContext context) { var shouldExecute = ExecutionCondition?.Invoke(model) ?? true; if (!shouldExecute) { return; } context.EnterPath(Path); if (ErrorId.HasValue) { context.EnableErrorDetectionMode(ErrorMode, ErrorId.Value); } RunValidation(model, context); context.LeavePath(); } protected abstract void RunValidation(T model, IValidationContext context); protected abstract void RunDiscovery(IDiscoveryContext context); } } ================================================ FILE: src/Validot/Validation/Scopes/ConvertedCommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System; internal class ConvertedCommandScope : CommandScope { public Converter Converter { get; set; } public int ScopeId { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterScope(ScopeId); } protected override void RunValidation(T model, IValidationContext context) { var convertedValue = Converter(model); context.EnterScope(ScopeId, convertedValue); } } } ================================================ FILE: src/Validot/Validation/Scopes/DictionaryCommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System; using System.Collections.Generic; internal class DictionaryCommandScope : CommandScope where T : IEnumerable> { public int ScopeId { get; set; } public Func KeyStringifier { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterCollectionItemPath(); context.EnterScope(ScopeId); context.LeavePath(); } protected override void RunValidation(T model, IValidationContext context) { foreach (var pair in model) { var keyRaw = KeyStringifier is null ? pair.Key as string : KeyStringifier(pair.Key); var keyNormalized = PathHelper.NormalizePath(keyRaw); context.EnterPath(keyNormalized); context.EnterScope(ScopeId, pair.Value); context.LeavePath(); if (context.ShouldFallBack) { break; } } } } } ================================================ FILE: src/Validot/Validation/Scopes/ICommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System; using Validot.Validation.Scopes.Builders; internal interface ICommandScope { } internal interface ICommandScope : ICommandScope, IScope { string Path { get; set; } int? ErrorId { get; set; } ErrorMode ErrorMode { get; set; } Predicate ExecutionCondition { get; set; } } } ================================================ FILE: src/Validot/Validation/Scopes/IDiscoverable.cs ================================================ namespace Validot.Validation.Scopes { internal interface IDiscoverable { void Discover(IDiscoveryContext context); } } ================================================ FILE: src/Validot/Validation/Scopes/IScope.cs ================================================ namespace Validot.Validation.Scopes { internal interface IScope : IValidatable, IDiscoverable { } } ================================================ FILE: src/Validot/Validation/Scopes/ISpecificationScope.cs ================================================ namespace Validot.Validation.Scopes { using System.Collections.Generic; internal interface ISpecificationScope : IValidatable, IDiscoverable { IReadOnlyList> CommandScopes { get; set; } Presence Presence { get; set; } int ForbiddenErrorId { get; set; } int RequiredErrorId { get; set; } } } ================================================ FILE: src/Validot/Validation/Scopes/IValidatable.cs ================================================ namespace Validot.Validation.Scopes { internal interface IValidatable { void Validate(T model, IValidationContext context); } } ================================================ FILE: src/Validot/Validation/Scopes/MemberCommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System; internal class MemberCommandScope : CommandScope { public Func GetMemberValue { get; set; } public int ScopeId { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterScope(ScopeId); } protected override void RunValidation(T model, IValidationContext context) { var memberValue = GetMemberValue(model); context.EnterScope(ScopeId, memberValue); } } } ================================================ FILE: src/Validot/Validation/Scopes/ModelCommandScope.cs ================================================ namespace Validot.Validation.Scopes { internal class ModelCommandScope : CommandScope { public int ScopeId { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterScope(ScopeId); } protected override void RunValidation(T model, IValidationContext context) { context.EnterScope(ScopeId, model); } } } ================================================ FILE: src/Validot/Validation/Scopes/NullableCommandScope.cs ================================================ namespace Validot.Validation.Scopes { internal class NullableCommandScope : CommandScope where T : struct { public int ScopeId { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterScope(ScopeId); } protected override void RunValidation(T? model, IValidationContext context) { if (model.HasValue) { context.EnterScope(ScopeId, model.Value); } } } } ================================================ FILE: src/Validot/Validation/Scopes/Presence.cs ================================================ namespace Validot.Validation.Scopes { internal enum Presence { Required = 0, Optional = 1, Forbidden = 2 } } ================================================ FILE: src/Validot/Validation/Scopes/RuleCommandScope.cs ================================================ namespace Validot.Validation.Scopes { using System; using Validot.Validation.Scopes.Builders; internal class RuleCommandScope : ICommandScope { public Predicate IsValid { get; set; } public int ErrorId { get; set; } = -1; public ErrorMode ErrorMode { get; set; } = ErrorMode.Override; public Predicate ExecutionCondition { get; set; } public string Path { get; set; } int? ICommandScope.ErrorId { get => ErrorId; set { if (!value.HasValue) { throw new InvalidOperationException($"{nameof(RuleCommandScope)} cannot have null {nameof(ErrorId)}"); } ErrorId = value.Value; } } public void Discover(IDiscoveryContext context) { context.EnterPath(Path); context.AddError(ErrorId); context.LeavePath(); } public void Validate(T model, IValidationContext context) { var shouldExecute = ExecutionCondition?.Invoke(model) ?? true; if (!shouldExecute) { return; } context.EnterPath(Path); if (!IsValid(model)) { context.AddError(ErrorId); } context.LeavePath(); } } } ================================================ FILE: src/Validot/Validation/Scopes/SpecificationScope.cs ================================================ namespace Validot.Validation.Scopes { using System.Collections.Generic; internal class SpecificationScope : ISpecificationScope { private static readonly bool IsNullable = default(T) == null; public IReadOnlyList> CommandScopes { get; set; } public Presence Presence { get; set; } public int ForbiddenErrorId { get; set; } public int RequiredErrorId { get; set; } public void Discover(IDiscoveryContext context) { if (IsNullable) { if (Presence == Presence.Forbidden) { context.AddError(ForbiddenErrorId, true); return; } if (Presence == Presence.Required) { context.AddError(RequiredErrorId, true); } } for (var i = 0; i < CommandScopes.Count; ++i) { CommandScopes[i].Discover(context); } } public void Validate(T model, IValidationContext context) { if (IsNullable) { if (model == null) { if (Presence == Presence.Required) { context.AddError(RequiredErrorId, true); } return; } if (Presence == Presence.Forbidden) { context.AddError(ForbiddenErrorId, true); return; } } for (var i = 0; i < CommandScopes.Count; ++i) { CommandScopes[i].Validate(model, context); if (context.ShouldFallBack) { return; } } } } } ================================================ FILE: src/Validot/Validation/Scopes/TypeCommandScope.cs ================================================ namespace Validot.Validation.Scopes { internal class TypeCommandScope : CommandScope { public int ScopeId { get; set; } protected override void RunDiscovery(IDiscoveryContext context) { context.EnterScope(ScopeId); } protected override void RunValidation(T model, IValidationContext context) { if (model is TType typedValue) { context.EnterScope(ScopeId, typedValue); } } } } ================================================ FILE: src/Validot/Validation/Stacks/PathStack.cs ================================================ namespace Validot.Validation.Stacks { using System.Collections.Generic; using System.Globalization; using System.Linq; internal class PathStack { private const int DefaultCollectionsCapacity = 30; private const int DefaultPreallocatedIndexes = 200; private static readonly IReadOnlyDictionary PreallocatedIndexes = Enumerable.Range(0, DefaultPreallocatedIndexes).ToDictionary(i => i, i => i.ToString(CultureInfo.InvariantCulture)); private readonly Stack _indexesLevelsStack = new Stack(DefaultCollectionsCapacity); private readonly Stack _indexesStack = new Stack(DefaultCollectionsCapacity); private readonly Stack _stack = new Stack(DefaultCollectionsCapacity); public string Path { get; private set; } = string.Empty; public int Level => _stack.Count; public bool HasIndexes => _indexesStack?.Count > 0; public IReadOnlyCollection IndexesStack => _indexesStack; public void Push(string path) { Path = path; _stack.Push(path); } public void PushWithIndex(string path, int index) { if (index < DefaultPreallocatedIndexes) { PushWithIndex(path, PreallocatedIndexes[index]); } else { PushWithIndex(path, index.ToString(CultureInfo.InvariantCulture)); } } public void PushWithDiscoveryIndex(string path) { PushWithIndex(path, PathHelper.CollectionIndexPrefixString); } public void Pop() { if (HasIndexes && _indexesLevelsStack.Peek() == Level) { _ = _indexesStack.Pop(); _ = _indexesLevelsStack.Pop(); } _ = _stack.Pop(); Path = _stack.Count > 0 ? _stack.Peek() : string.Empty; } private void PushWithIndex(string path, string index) { Push(path); _indexesStack.Push(index); _indexesLevelsStack.Push(Level); } } } ================================================ FILE: src/Validot/Validation/Stacks/ReferenceLoopException.cs ================================================ namespace Validot.Validation.Stacks { using System; public sealed class ReferenceLoopException : ValidotException { internal ReferenceLoopException(string path, string nestedPath, int scopeId, Type type) : base(GetMessage(path, nestedPath, type)) { Path = path; NestedPath = nestedPath; Type = type; ScopeId = scopeId; } internal ReferenceLoopException(int scopeId, Type type) : base($"Reference loop detected: object of type {type.GetFriendlyName()} has been detected twice in the reference graph, effectively creating the infinite references loop (where exactly, that information is not available - is that validation comes from IsValid method, please repeat it using the Validate method and examine the exception thrown)") { Path = null; NestedPath = null; Type = type; ScopeId = scopeId; } public int ScopeId { get; } public Type Type { get; } public string Path { get; } public string NestedPath { get; } private static string GetMessage(string path, string infiniteLoopNestedPath, Type type) { var pathStringified = string.IsNullOrEmpty(path) ? "the root path, so the validated object itself," : $"the path '{path}'"; return $"Reference loop detected: object of type {type.GetFriendlyName()} has been detected twice in the reference graph, effectively creating an infinite references loop (at first under {pathStringified} and then under the nested path '{infiniteLoopNestedPath}')"; } } } ================================================ FILE: src/Validot/Validation/Stacks/ReferenceLoopProtectionSettings.cs ================================================ namespace Validot.Validation.Stacks { internal class ReferenceLoopProtectionSettings { public ReferenceLoopProtectionSettings(object rootModelReference = null) { RootModelReference = rootModelReference; } public object RootModelReference { get; } } } ================================================ FILE: src/Validot/Validation/Stacks/ReferencesStack.cs ================================================ namespace Validot.Validation.Stacks { using System.Collections.Generic; internal class ReferencesStack { private const int InitPathsCapacity = 4; private const int InitScopesCapacity = 2; private readonly Dictionary> _dictionary = new Dictionary>(InitPathsCapacity); public bool TryPush(int scopeId, string path, object modelReference, out string higherLevelPath) { if (!_dictionary.ContainsKey(scopeId)) { _dictionary.Add(scopeId, new Stack(InitScopesCapacity)); } if (_dictionary[scopeId].Count > 0) { foreach (var stackItem in _dictionary[scopeId]) { if (ReferenceEquals(stackItem.Reference, modelReference)) { higherLevelPath = stackItem.Path; return false; } } } var item = new StackItem { Path = path, Reference = modelReference }; _dictionary[scopeId].Push(item); higherLevelPath = null; return true; } public object Pop(int scopeId, out string path) { var item = _dictionary[scopeId].Pop(); path = item.Path; return item.Reference; } public int GetStoredReferencesCount() { var count = 0; foreach (var pair in _dictionary) { count += pair.Value.Count; } return count; } private class StackItem { public object Reference { get; set; } public string Path { get; set; } } } } ================================================ FILE: src/Validot/Validation/ValidationContext.cs ================================================ namespace Validot.Validation { using System; using System.Collections.Generic; using System.Linq; using Validot.Validation.Scheme; using Validot.Validation.Scopes.Builders; using Validot.Validation.Stacks; internal class ValidationContext : IValidationContext, IErrorsHolder { private readonly ErrorFlag _appendingErrorFlag = new ErrorFlag(10); private readonly ErrorFlag _overridingErrorFlag = new ErrorFlag(10); private readonly IModelScheme _modelScheme; private readonly PathStack _pathStack = new PathStack(); private readonly ReferencesStack _referencesStack; private readonly bool _referencesLoopProtectionEnabled; public ValidationContext(IModelScheme modelScheme, bool failFast, ReferenceLoopProtectionSettings referenceLoopProtectionSettings) { _modelScheme = modelScheme; FailFast = failFast; ReferenceLoopProtectionSettings = referenceLoopProtectionSettings; if (!(referenceLoopProtectionSettings is null)) { _referencesLoopProtectionEnabled = true; _referencesStack = new ReferencesStack(); if (_modelScheme.RootModelType.IsClass && !(referenceLoopProtectionSettings.RootModelReference is null)) { _ = _referencesStack.TryPush(_modelScheme.RootSpecificationScopeId, GetCurrentPath(), referenceLoopProtectionSettings.RootModelReference, out _); } } } public ReferenceLoopProtectionSettings ReferenceLoopProtectionSettings { get; } public bool FailFast { get; } public Dictionary> Errors { get; private set; } public bool ShouldFallBack => (FailFast && !(Errors is null) && Errors.Count > 0) || _overridingErrorFlag.IsDetectedAtAnyLevel; public void AddError(int errorId, bool skipIfDuplicateInPath = false) { if (_overridingErrorFlag.IsEnabledAtAnyLevel) { _overridingErrorFlag.SetDetected(_pathStack.Level); return; } _appendingErrorFlag.SetDetected(_pathStack.Level); SaveError(errorId, skipIfDuplicateInPath); } public void EnableErrorDetectionMode(ErrorMode errorMode, int errorId) { if (_overridingErrorFlag.IsEnabledAtAnyLevel) { return; } var flag = errorMode == ErrorMode.Append ? _appendingErrorFlag : _overridingErrorFlag; flag.SetEnabled(_pathStack.Level, errorId); } public void LeavePath() { if (_overridingErrorFlag.LeaveLevelAndTryGetError(_pathStack.Level, out var overridingErrorId)) { SaveError(overridingErrorId, false); } if (_appendingErrorFlag.LeaveLevelAndTryGetError(_pathStack.Level, out var appendingErrorId)) { SaveError(appendingErrorId, false); } _pathStack.Pop(); } public void EnterCollectionItemPath(int i) { var resolvedPath = _modelScheme.ResolvePath(_pathStack.Path, PathHelper.CollectionIndexPrefixString); _pathStack.PushWithIndex(resolvedPath, i); } public void EnterPath(string path) { var resolvedPath = path != null ? _modelScheme.ResolvePath(_pathStack.Path, path) : _pathStack.Path; _pathStack.Push(resolvedPath); } public void EnterScope(int scopeId, T model) { var useReferenceLoopProtection = typeof(T).IsClass && _referencesLoopProtectionEnabled; if (useReferenceLoopProtection && !_referencesStack.TryPush(scopeId, _pathStack.Path, model, out var higherLevelPath)) { FailWithException(higherLevelPath, scopeId, typeof(T)); return; } var specificationScope = _modelScheme.GetSpecificationScope(scopeId); specificationScope.Validate(model, this); if (useReferenceLoopProtection) { _ = _referencesStack.Pop(scopeId, out _); } } public int? GetLoopProtectionReferencesStackCount() { return _referencesStack?.GetStoredReferencesCount(); } private string GetCurrentPath() { return _pathStack.HasIndexes ? _modelScheme.GetPathWithIndexes(_pathStack.Path, _pathStack.IndexesStack) : _pathStack.Path; } private void FailWithException(string higherLevelPath, int scopeId, Type type) { if (_pathStack.HasIndexes) { var higherLevelPathIndexesAmount = PathHelper.GetIndexesAmount(higherLevelPath); if (higherLevelPathIndexesAmount > 0) { var stack = higherLevelPathIndexesAmount == _pathStack.IndexesStack.Count ? _pathStack.IndexesStack : _pathStack.IndexesStack.Skip(_pathStack.IndexesStack.Count - higherLevelPathIndexesAmount).ToList(); higherLevelPath = _modelScheme.GetPathWithIndexes(higherLevelPath, stack); } } throw new ReferenceLoopException(higherLevelPath, GetCurrentPath(), scopeId, type); } private void SaveError(int errorId, bool skipIfDuplicateInPath) { if (Errors is null) { Errors = new Dictionary>(1); } var currentPath = GetCurrentPath(); if (!Errors.ContainsKey(currentPath)) { Errors.Add(currentPath, new List(1)); } if (skipIfDuplicateInPath && Errors[currentPath].Contains(errorId)) { return; } Errors[currentPath].Add(errorId); } } } ================================================ FILE: src/Validot/Validator.cs ================================================ namespace Validot { using System; using System.Linq; using Validot.Errors; using Validot.Factory; using Validot.Results; using Validot.Settings; using Validot.Validation; using Validot.Validation.Scheme; using Validot.Validation.Stacks; public abstract class Validator { /// /// Gets validator factory - the recommended way of creating instances of . /// public static ValidatorFactory Factory { get; } = new ValidatorFactory(); } /// public sealed class Validator : Validator, IValidator { private readonly IMessageService _messageService; private readonly IModelScheme _modelScheme; internal Validator(IModelScheme modelScheme, IValidatorSettings settings) { Settings = settings ?? throw new ArgumentNullException(nameof(settings)); _modelScheme = modelScheme ?? throw new ArgumentNullException(nameof(modelScheme)); _messageService = new MessageService(Settings.Translations, _modelScheme.ErrorRegistry, _modelScheme.Template); Template = new ValidationResult(_modelScheme.Template.ToDictionary(p => p.Key, p => p.Value.ToList()), _modelScheme.ErrorRegistry, _messageService); } /// public IValidatorSettings Settings { get; } /// public IValidationResult Template { get; } /// public bool IsValid(T model) { var validationContext = new IsValidValidationContext(_modelScheme, Settings.ReferenceLoopProtectionEnabled ? new ReferenceLoopProtectionSettings(model) : null); _modelScheme.RootSpecificationScope.Validate(model, validationContext); return !validationContext.ErrorFound; } /// public IValidationResult Validate(T model, bool failFast = false) { var validationContext = new ValidationContext(_modelScheme, failFast, Settings.ReferenceLoopProtectionEnabled ? new ReferenceLoopProtectionSettings(model) : null); _modelScheme.RootSpecificationScope.Validate(model, validationContext); var isValid = validationContext.Errors is null; return isValid ? ValidationResult.NoErrorsResult : new ValidationResult(validationContext.Errors, _modelScheme.ErrorRegistry, _messageService); } } } ================================================ FILE: src/Validot/Validot.csproj ================================================ netstandard2.0 Validot Bartosz Lenar https://github.com/bartoszlenar/Validot git https://github.com/bartoszlenar/Validot.git MIT assets/logo/validot-icon.png Validot is a performance-first, compact library for advanced model validation. Using a simple declarative fluent interface, it efficiently handles classes, structs, nested members, collections, nullables, plus any relation or combination of them. It also supports translations, custom logic extensions with tests, and DI containers. validation validation-library validator specification fluent-api model-validation false true $(NoWarn);1591 true true true true snupkg runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: src/Validot/ValidotException.cs ================================================ namespace Validot { using System; public class ValidotException : Exception { public ValidotException(string message) : base(message) { } public ValidotException(string message, Exception innerException) : base(message, innerException) { } } } ================================================ FILE: tests/AssemblyWithHolders/AssemblyWithHolders.csproj ================================================ netstandard2.0 ================================================ FILE: tests/AssemblyWithHolders/AssemblyWithHoldersHook.cs ================================================ namespace AssemblyWithHolders { public class AssemblyWithHoldersHook { } } ================================================ FILE: tests/AssemblyWithHolders/HolderOfDecimalSpecification.cs ================================================ namespace AssemblyWithHolders { using Validot; public class HolderOfDecimalSpecification : ISpecificationHolder { public Specification Specification { get; } = s => s .GreaterThanOrEqualTo(1).WithMessage("Min value is 1") .LessThanOrEqualTo(10).WithMessage("Max value is 10"); } } ================================================ FILE: tests/AssemblyWithHolders/HolderOfIntSpecificationAndSettings.cs ================================================ namespace AssemblyWithHolders { using System; using Validot; using Validot.Factory; using Validot.Settings; public class HolderOfIntSpecificationAndSettings : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; } = s => s .GreaterThanOrEqualTo(1).WithMessage("Min value is 1") .LessThanOrEqualTo(10).WithMessage("Max value is 10"); public Func Settings { get; } = s => s .WithTranslation("English", "Min value is 1", "The minimum value is 1") .WithTranslation("English", "Max value is 10", "The maximum value is 10") .WithTranslation("BinaryEnglish", "Min value is 1", "The minimum value is 0b0001") .WithTranslation("BinaryEnglish", "Max value is 10", "The maximum value is 0b1010") .WithReferenceLoopProtection(); } } ================================================ FILE: tests/AssemblyWithHolders/HolderOfMultipleSpecifications.cs ================================================ namespace AssemblyWithHolders { using System; using Validot; public class HolderOfMultipleSpecifications : ISpecificationHolder, ISpecificationHolder { Specification ISpecificationHolder.Specification { get; } = s => s .AfterOrEqualTo(new DateTime(2000, 1, 1), TimeComparison.JustDate).WithMessage("Dates after 1st of Jan'00 are allowed") .Before(new DateTime(2021, 1, 1), TimeComparison.JustDate).WithMessage("Dates before 1st of Jan'21 are allowed"); Specification ISpecificationHolder.Specification { get; } = s => s .AfterOrEqualTo(new DateTimeOffset(2000, 1, 1, 1, 1, 1, TimeSpan.Zero)).WithMessage("Dates after midnight 1st of Jan'00 are allowed") .Before(new DateTimeOffset(2021, 1, 1, 1, 1, 1, TimeSpan.Zero)).WithMessage("Dates before midnight 1st of Jan'21 are allowed"); } } ================================================ FILE: tests/AssemblyWithHolders/HolderOfMultipleSpecificationsAndSettings.cs ================================================ namespace AssemblyWithHolders { using System; using Validot; using Validot.Factory; using Validot.Settings; public class HolderOfMultipleSpecificationsAndSettings : ISpecificationHolder, ISpecificationHolder, ISettingsHolder { Specification ISpecificationHolder.Specification => s => s .GreaterThan(1).WithMessage("Min value is 1") .LessThan(10).WithMessage("Max value is 10"); Specification ISpecificationHolder.Specification => s => s .GreaterThan(1).WithMessage("Min value is 1") .LessThan(10).WithMessage("Max value is 10"); public Func Settings { get; } = s => s .WithTranslation("English", "Min value is 1", "Minimum value is 1") .WithTranslation("English", "Max value is 10", "Maximum value is 10"); } } ================================================ FILE: tests/AssemblyWithHolders/HolderOfStringSpecification.cs ================================================ namespace AssemblyWithHolders { using Validot; public class HolderOfStringSpecification : ISpecificationHolder { public Specification Specification { get; } = s => s .NotEmpty().WithMessage("Empty text not allowed") .MinLength(3).WithMessage("Text shorter than 3 characters not allowed") .MaxLength(10).WithMessage("Text longer than 10 characters not allowed") .NotContains("!").WithMessage("Text containing exclamation mark not allowed"); } } ================================================ FILE: tests/AssemblyWithHolders/HolderOfStringSpecificationAndSettings.cs ================================================ namespace AssemblyWithHolders { using System; using Validot; using Validot.Factory; using Validot.Settings; public class HolderOfStringSpecificationAndSettings : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; } = s => s .NotEmpty().WithMessage("Empty text not allowed") .MinLength(3).WithMessage("Text shorter than 3 characters not allowed") .MaxLength(10).WithMessage("Text longer than 10 characters not allowed") .NotContains("!").WithMessage("Text containing exclamation mark not allowed"); public Func Settings { get; } = s => s .WithReferenceLoopProtection() .WithTranslation("English", "Empty text not allowed", "Empty string is invalid!") .WithTranslation("English", "Text shorter than 3 characters not allowed", "Only strings of length from 3 to 10 are allowed") .WithTranslation("English", "Text longer than 10 characters not allowed", "Only strings of length from 3 to 10 are allowed"); } } ================================================ FILE: tests/AssemblyWithHolders/NestedHolders.cs ================================================ // unset namespace AssemblyWithHolders { using System; using Validot; using Validot.Factory; using Validot.Settings; public class NestedHolders { public class NestedHolderOfBoolSpecification : ISpecificationHolder { public Specification Specification { get; } = s => s.True().WithMessage("Must be true"); } public class NestedHolderOfStringSpecification : ISpecificationHolder { public Specification Specification { get; } = s => s.NotEmpty(); } public class NestedHolderOfStringSpecificationAndSettings : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; } = s => s.NotEmpty(); public Func Settings { get; } = s => s.WithTranslation("English", "Texts.NotEmpty", "Cannot be empty!"); } } } ================================================ FILE: tests/AssemblyWithHolders/PrivateSpecificationAndSettingsHolder.cs ================================================ namespace AssemblyWithHolders { using System; using Validot; using Validot.Factory; using Validot.Settings; internal class PrivateSpecificationAndSettingsHolder : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; } = s => s.NotEmpty(); public Func Settings { get; } = s => s.WithReferenceLoopProtection(); } } ================================================ FILE: tests/AssemblyWithHolders/PrivateSpecificationHolder.cs ================================================ namespace AssemblyWithHolders { using Validot; internal class PrivateSpecificationHolder : ISpecificationHolder { public Specification Specification { get; } = s => s.NotEmpty(); } } ================================================ FILE: tests/AssemblyWithHolders/Properties/AssemblyInfo.cs ================================================ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Validot.Tests.Unit")] ================================================ FILE: tests/Validot.Benchmarks/.editorconfig ================================================ # C# files [*.cs] dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1852.severity = none ================================================ FILE: tests/Validot.Benchmarks/Comparisons/ComparisonDataSet.cs ================================================ namespace Validot.Benchmarks.Comparisons { using System; using System.Collections.Generic; using System.Linq; using Bogus; using Bogus.Extensions; using FluentValidation; public static class ComparisonDataSet { static ComparisonDataSet() { void SetupValidotSpecifications() { NestedModelSpecification = _ => _ .Member(m => m.Number1, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message N1")) .Member(m => m.Number2, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message N2")) .Member(m => m.SuperNumber1, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message S1")) .Member(m => m.SuperNumber2, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message S2")) .Member(m => m.Text1, m => m.Rule(v => v.Contains('a', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Nested Message T1")) .Member(m => m.Text2, m => m.Rule(v => v.Contains('b', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Nested Message T2")); FullModelSpecification = _ => _ .Member(m => m.Text1, m => m.Rule(v => v.Contains('a', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T1")) .Member(m => m.Text2, m => m.Rule(v => v.Contains('b', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T2")) .Member(m => m.Text3, m => m.Rule(v => v.Contains('c', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T3")) .Member(m => m.Text4, m => m.Rule(v => v.Contains('d', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T4")) .Member(m => m.Text5, m => m.Rule(v => v.Contains('e', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T5")) .Member(m => m.Number1, m2 => m2.Rule(v => v < 10).WithMessage("Message N1")) .Member(m => m.Number2, m2 => m2.Rule(v => v < 10).WithMessage("Message N2")) .Member(m => m.Number3, m2 => m2.Rule(v => v < 10).WithMessage("Message N3")) .Member(m => m.Number4, m2 => m2.Rule(v => v < 10).WithMessage("Message N4")) .Member(m => m.Number5, m2 => m2.Rule(v => v < 10).WithMessage("Message N5")) .Member(m => m.SuperNumber1, m2 => m2.Rule(v => v < 10).WithMessage("Message S1")) .Member(m => m.SuperNumber2, m2 => m2.Rule(v => v < 10).WithMessage("Message S2")) .Member(m => m.SuperNumber3, m2 => m2.Rule(v => v < 10).WithMessage("Message S3")) .Member(m => m.NestedModel1, NestedModelSpecification) .Member(m => m.NestedModel2, NestedModelSpecification) .Member(m => m.ModelCollection, m => m .MaxCollectionSize(10).WithMessage("No more than 10 items are allowed") .AsCollection(NestedModelSpecification)) .Member(m => m.StructCollection, m => m .MaxCollectionSize(10).WithMessage("No more than 10 items are allowed") .AsCollection(m1 => m1.Rule(m2 => m2 < 10).WithMessage("Message C"))); } void SetupFullModelManyErrorsFaker() { var nestedModelManyErrorsFaker = new Faker() .RuleFor(m => m.Number1, m => m.Random.Int(0, 5)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 15)) .RuleFor(m => m.Text1, m => m.Lorem.Word().OrNull(m, 0.1f)) .RuleFor(m => m.Text2, m => m.Lorem.Word().OrNull(m, 0.15f)) .RuleFor(m => m.SuperNumber1, m => m.Random.Number(0, 20).OrNull(m, 0.20f)) .RuleFor(m => m.SuperNumber2, m => m.Random.Number(0, 20).OrNull(m, 0.25f)); FullModelManyErrorsFaker = new Faker() .RuleFor(m => m.Text1, m => m.Lorem.Word().OrNull(m, 0.1f)) .RuleFor(m => m.Text2, m => m.Lorem.Word().OrNull(m, 0.15f)) .RuleFor(m => m.Text3, m => m.Lorem.Word().OrNull(m, 0.2f)) .RuleFor(m => m.Text4, m => m.Lorem.Word().OrNull(m, 0.25f)) .RuleFor(m => m.Text5, m => m.Lorem.Word().OrNull(m, 0.3f)) .RuleFor(m => m.Number1, m => m.Random.Int(0, 5)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 10)) .RuleFor(m => m.Number3, m => m.Random.Int(0, 15)) .RuleFor(m => m.Number4, m => m.Random.Int(0, 20)) .RuleFor(m => m.Number5, m => m.Random.Int(0, 25)) .RuleFor(m => m.SuperNumber1, m => m.Random.Decimal(0, 20).OrNull(m, 0.20f)) .RuleFor(m => m.SuperNumber2, m => m.Random.Decimal(0, 20).OrNull(m, 0.25f)) .RuleFor(m => m.SuperNumber2, m => m.Random.Decimal(0, 20).OrNull(m, 0.30f)) .RuleFor(m => m.NestedModel1, m => nestedModelManyErrorsFaker.Generate()) .RuleFor(m => m.NestedModel2, m => nestedModelManyErrorsFaker.Generate()) .RuleFor(m => m.ModelCollection, m => nestedModelManyErrorsFaker.GenerateBetween(0, 20).ToList().OrNull(m, 0.7f)) .RuleFor(m => m.StructCollection, m => Enumerable.Range(1, m.Random.Int(1, 20)).Select(_ => m.Random.Number(0, 20)).ToList().OrNull(m, 0.7f)); } void SetupFullModelNoErrorsFaker() { var nestedModelNoErrorsFaker = new Faker() .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Text1, m => m.Lorem.Word() + "a") .RuleFor(m => m.Text2, m => m.Lorem.Word() + "b") .RuleFor(m => m.SuperNumber1, m => m.Random.Number(0, 9)) .RuleFor(m => m.SuperNumber2, m => m.Random.Number(0, 9)); FullModelNoErrorsFaker = new Faker() .RuleFor(m => m.Text1, m => m.Lorem.Word() + "a") .RuleFor(m => m.Text2, m => m.Lorem.Word() + "b") .RuleFor(m => m.Text3, m => m.Lorem.Word() + "c") .RuleFor(m => m.Text4, m => m.Lorem.Word() + "d") .RuleFor(m => m.Text5, m => m.Lorem.Word() + "e") .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number3, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number4, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number5, m => m.Random.Int(0, 9)) .RuleFor(m => m.SuperNumber1, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber2, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber3, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.NestedModel1, m => nestedModelNoErrorsFaker.Generate()) .RuleFor(m => m.NestedModel2, m => nestedModelNoErrorsFaker.Generate()) .RuleFor(m => m.ModelCollection, m => nestedModelNoErrorsFaker.GenerateBetween(0, 9).ToList()) .RuleFor(m => m.StructCollection, m => Enumerable.Range(1, m.Random.Int(1, 9)).Select(_ => m.Random.Number(0, 9)).ToList()); } void SetupFullModelHalfErrorsFaker() { var nestedModelsHalfErrorsFaker = new Faker() .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Text1, m => string.Join(" ", m.Lorem.Words(20))) .RuleFor(m => m.Text2, m => string.Join("b", m.Lorem.Words(15))) .RuleFor(m => m.SuperNumber1, m => m.Random.Number(0, 10)) .RuleFor(m => m.SuperNumber2, m => m.Random.Number(0, 9).OrNull(m, 0.01f)); FullModelHalfErrorsFaker = new Faker() .RuleFor(m => m.Text1, m => string.Join("a", m.Lorem.Words(10))) .RuleFor(m => m.Text2, m => string.Join("b", m.Lorem.Words(10))) .RuleFor(m => m.Text3, m => string.Join("c", m.Lorem.Words(10))) .RuleFor(m => m.Text4, m => string.Join("d", m.Lorem.Words(10))) .RuleFor(m => m.Text5, m => string.Join(" ", m.Lorem.Words(20))) .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number3, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number4, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number5, m => m.Random.Int(0, 10)) .RuleFor(m => m.SuperNumber1, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber2, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber3, m => m.Random.Decimal(0, 10).OrNull(m, 0.01f)) .RuleFor(m => m.NestedModel1, m => nestedModelsHalfErrorsFaker.Generate()) .RuleFor(m => m.NestedModel2, m => nestedModelsHalfErrorsFaker.Generate()) .RuleFor(m => m.ModelCollection, m => nestedModelsHalfErrorsFaker.GenerateBetween(0, 9).ToList()) .RuleFor(m => m.StructCollection, m => Enumerable.Range(1, m.Random.Int(1, 11)).Select(_ => m.Random.Number(0, 9)).ToList()); } SetupFullModelHalfErrorsFaker(); SetupValidotSpecifications(); SetupFullModelManyErrorsFaker(); SetupFullModelNoErrorsFaker(); Size = 10_000; Randomizer.Seed = new Random(666); ManyErrorsDataSet = FullModelManyErrorsFaker.GenerateLazy(Size).ToList(); HalfErrorsDataSet = FullModelHalfErrorsFaker.GenerateLazy(Size).ToList(); NoErrorsDataSet = FullModelNoErrorsFaker.GenerateLazy(Size).ToList(); DataSets = new Dictionary>(3) { ["ManyErrors"] = ManyErrorsDataSet, ["HalfErrors"] = HalfErrorsDataSet, ["NoErrors"] = NoErrorsDataSet }; } public static int Size { get; } public static IReadOnlyList ManyErrorsDataSet { get; } public static IReadOnlyList HalfErrorsDataSet { get; } public static IReadOnlyList NoErrorsDataSet { get; } public static IReadOnlyDictionary> DataSets { get; } public class FullModel { public string Text1 { get; set; } public string Text2 { get; set; } public string Text3 { get; set; } public string Text4 { get; set; } public string Text5 { get; set; } public int Number1 { get; set; } public int Number2 { get; set; } public int Number3 { get; set; } public int Number4 { get; set; } public int Number5 { get; set; } public decimal? SuperNumber1 { get; set; } public decimal? SuperNumber2 { get; set; } public decimal? SuperNumber3 { get; set; } public NestedModel NestedModel1 { get; set; } public NestedModel NestedModel2 { get; set; } public IReadOnlyList ModelCollection { get; set; } public IReadOnlyList StructCollection { get; set; } } public class NestedModel { public string Text1 { get; set; } public string Text2 { get; set; } public int Number1 { get; set; } public int Number2 { get; set; } public decimal? SuperNumber1 { get; set; } public decimal? SuperNumber2 { get; set; } } public class FullModelValidator : AbstractValidator { public FullModelValidator() { RuleFor(x => x.Text1).NotNull(); RuleFor(x => x.Text1).Must(m => m.Contains('a', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T1").When(m => m.Text1 != null); RuleFor(x => x.Text2).NotNull(); RuleFor(x => x.Text2).Must(m => m.Contains('b', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T2").When(m => m.Text2 != null); RuleFor(x => x.Text3).NotNull(); RuleFor(x => x.Text3).Must(m => m.Contains('c', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T3").When(m => m.Text3 != null); RuleFor(x => x.Text4).NotNull(); RuleFor(x => x.Text4).Must(m => m.Contains('d', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T4").When(m => m.Text4 != null); RuleFor(x => x.Text5).NotNull(); RuleFor(x => x.Text5).Must(m => m.Contains('e', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T5").When(m => m.Text5 != null); RuleFor(x => x.Number1).Must(m => m < 10).WithMessage("Message N1"); RuleFor(x => x.Number2).Must(m => m < 10).WithMessage("Message N2"); RuleFor(x => x.Number3).Must(m => m < 10).WithMessage("Message N3"); RuleFor(x => x.Number4).Must(m => m < 10).WithMessage("Message N4"); RuleFor(x => x.Number5).Must(m => m < 10).WithMessage("Message N5"); RuleFor(x => x.SuperNumber1).NotNull(); RuleFor(x => x.SuperNumber1).Must(m => m < 10).WithMessage("Message S1").When(m => m.SuperNumber1 != null); RuleFor(x => x.SuperNumber2).NotNull(); RuleFor(x => x.SuperNumber2).Must(m => m < 10).WithMessage("Message S2").When(m => m.SuperNumber2 != null); RuleFor(x => x.SuperNumber3).NotNull(); RuleFor(x => x.SuperNumber3).Must(m => m < 10).WithMessage("Message S3").When(m => m.SuperNumber3 != null); RuleFor(x => x.NestedModel1).NotNull(); RuleFor(x => x.NestedModel1).SetValidator(new NestedModelValidator()).When(m => m.NestedModel1 != null); RuleFor(x => x.NestedModel2).NotNull(); RuleFor(x => x.NestedModel2).SetValidator(new NestedModelValidator()).When(m => m.NestedModel2 != null); RuleFor(x => x.ModelCollection).NotNull(); RuleFor(x => x.ModelCollection) .Must(x => x.Count <= 10).WithMessage("No more than 10 items are allowed") .When(m => m.ModelCollection != null); RuleForEach(x => x.ModelCollection).SetValidator(new NestedModelValidator()).When(m => m.ModelCollection != null); RuleFor(x => x.StructCollection).NotNull(); RuleFor(x => x.StructCollection) .Must(x => x.Count <= 10).WithMessage("No more than 10 items are allowed") .When(m => m.StructCollection != null); RuleForEach(x => x.StructCollection).Must(m1 => m1 <= 10).WithMessage("Message C").When(m => m.StructCollection != null); } } public class NestedModelValidator : AbstractValidator { public NestedModelValidator() { RuleFor(x => x.Text1).NotNull(); RuleFor(x => x.Text1).Must(m => m.Contains('a', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Nested Message T1").When(m => m.Text1 != null); RuleFor(x => x.Text2).NotNull(); RuleFor(x => x.Text2).Must(m => m.Contains('b', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Nested Message T2").When(m => m.Text2 != null); RuleFor(x => x.Number1).Must(m => m < 10).WithMessage("Nested Message N1"); RuleFor(x => x.Number2).Must(m => m < 10).WithMessage("Nested Message N2"); RuleFor(x => x.SuperNumber1).NotNull(); RuleFor(x => x.SuperNumber1).Must(m => m < 10).WithMessage("Nested Message S1").When(m => m.SuperNumber1 != null); RuleFor(x => x.SuperNumber2).NotNull(); RuleFor(x => x.SuperNumber2).Must(m => m < 10).WithMessage("Nested Message S2").When(m => m.SuperNumber2 != null); } } public static Specification FullModelSpecification { get; private set; } public static Specification NestedModelSpecification { get; private set;} public static Faker FullModelManyErrorsFaker { get; private set;} public static Faker FullModelNoErrorsFaker { get; private set;} public static Faker FullModelHalfErrorsFaker { get; private set;} } } ================================================ FILE: tests/Validot.Benchmarks/Comparisons/EngineOnlyBenchmark.cs ================================================ namespace Validot.Benchmarks.Comparisons { using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Attributes; using FluentValidation; [MemoryDiagnoser] public class EngineOnlyBenchmark { private IReadOnlyList _noLogicModels; private Validot.IValidator _validotSingleRuleValidator; private Validot.IValidator _validotTenRulesValidator; private NoLogicModelSingleRuleValidator _fluentValidationSingleRuleValidator; private NoLogicModelTenRulesValidator _fluentValidationTenRulesValidator; public class VoidModel { public object Member { get; set; } } public class NoLogicModelSingleRuleValidator : AbstractValidator { public NoLogicModelSingleRuleValidator() { RuleFor(m => m.Member).Must(o => true); } } public class NoLogicModelTenRulesValidator : AbstractValidator { public NoLogicModelTenRulesValidator() { RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); RuleFor(m => m.Member).Must(o => true); } } [Params(10000)] public int N { get; set; } [GlobalSetup] public void GlobalSetup() { _validotSingleRuleValidator = Validator.Factory.Create(_ => _ .Member(m => m.Member, m => m.Optional().Rule(n => true)) ); _validotTenRulesValidator = Validator.Factory.Create(_ => _ .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) .Member(m => m.Member, m => m.Optional().Rule(n => true)) ); _fluentValidationSingleRuleValidator = new NoLogicModelSingleRuleValidator(); _fluentValidationTenRulesValidator = new NoLogicModelTenRulesValidator(); _noLogicModels = Enumerable.Range(0, N).Select(m => new VoidModel() { Member = new object() }).ToList(); } [Benchmark] public bool IsValid_SingleRule_FluentValidation() { var t = true; for(var i = 0; i < N; ++i) { t = _fluentValidationSingleRuleValidator.Validate(_noLogicModels[i]).IsValid; } return t; } [Benchmark] public bool IsValid_TenRules_FluentValidation() { var t = true; for(var i = 0; i < N; ++i) { t = _fluentValidationTenRulesValidator.Validate(_noLogicModels[i]).IsValid; } return t; } [Benchmark] public bool IsValid_SingleRule_Validot() { var t = true; for(var i = 0; i < N; ++i) { t = _validotSingleRuleValidator.IsValid(_noLogicModels[i]); } return t; } [Benchmark] public bool IsValid_TenRules_Validot() { var t = true; for(var i = 0; i < N; ++i) { t = _validotTenRulesValidator.IsValid(_noLogicModels[i]); } return t; } [Benchmark] public object Validate_SingleRule_FluentValidation() { object t = null; for(var i = 0; i < N; ++i) { t = _fluentValidationSingleRuleValidator.Validate(_noLogicModels[i]); } return t; } [Benchmark] public object Validate_TenRules_FluentValidation() { object t = null; for(var i = 0; i < N; ++i) { t = _fluentValidationTenRulesValidator.Validate(_noLogicModels[i]); } return t; } [Benchmark] public object Validate_SingleRule_Validot() { object t = null; for(var i = 0; i < N; ++i) { t = _validotSingleRuleValidator.Validate(_noLogicModels[i]); } return t; } [Benchmark] public object Validate_TenRules_Validot() { object t = null; for(var i = 0; i < N; ++i) { t = _validotSingleRuleValidator.Validate(_noLogicModels[i]); } return t; } } } ================================================ FILE: tests/Validot.Benchmarks/Comparisons/InitializationBenchmark.cs ================================================ namespace Validot.Benchmarks.Comparisons { using BenchmarkDotNet.Attributes; [MemoryDiagnoser] public class InitializationBenchmark { [Benchmark] public object Initialization_FluentValidation() { return new ComparisonDataSet.FullModelValidator(); } [Benchmark] public object Initialization_Validot() { return Validator.Factory.Create(ComparisonDataSet.FullModelSpecification); } } } ================================================ FILE: tests/Validot.Benchmarks/Comparisons/ReportingBenchmark.cs ================================================ namespace Validot.Benchmarks.Comparisons { using System.Collections.Generic; using BenchmarkDotNet.Attributes; using FluentValidation; [MemoryDiagnoser] public class ReportingBenchmark { private Validot.IValidator _validotValidator; private ComparisonDataSet.FullModelValidator _fluentValidationValidator; private IReadOnlyDictionary> _dataSets; [Params("ManyErrors", "HalfErrors")] public string DataSet { get; set; } [GlobalSetup] public void GlobalSetup() { _fluentValidationValidator = new ComparisonDataSet.FullModelValidator(); _validotValidator = Validator.Factory.Create(ComparisonDataSet.FullModelSpecification); _dataSets = ComparisonDataSet.DataSets; } [Benchmark] public object Reporting_FluentValidation() { var models = _dataSets[DataSet]; var t = new object(); for(var i = 0; i < models.Count; ++i) { var result = _fluentValidationValidator.Validate(models[i]); if (!result.IsValid) { t = result.ToString(); } } return t; } [Benchmark] public object Reporting_Validot() { var models = _dataSets[DataSet]; var t = new object(); for(var i = 0; i < models.Count; ++i) { var model = models[i]; if (!_validotValidator.IsValid(model)) { t = _validotValidator.Validate(model).ToString(); } } return t; } } } ================================================ FILE: tests/Validot.Benchmarks/Comparisons/ToStringBenchmark.cs ================================================ namespace Validot.Benchmarks.Comparisons { using System; using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Attributes; using Bogus; using FluentValidation; using Validot.Results; using ValidationResult = FluentValidation.Results.ValidationResult; [MemoryDiagnoser] public class ToStringBenchmark { private IReadOnlyDictionary> _validotResults; private IReadOnlyDictionary> _fluentValidationResults; [Params("ManyErrors", "HalfErrors", "NoErrors")] public string DataSet { get; set; } [GlobalSetup] public void GlobalSetup() { var fluentValidationValidator = new ComparisonDataSet.FullModelValidator(); var validotValidator = Validator.Factory.Create(ComparisonDataSet.FullModelSpecification); _validotResults = new Dictionary>() { ["ManyErrors"] = GetValidotResults(ComparisonDataSet.ManyErrorsDataSet), ["HalfErrors"] = GetValidotResults(ComparisonDataSet.HalfErrorsDataSet), ["NoErrors"] = GetValidotResults(ComparisonDataSet.NoErrorsDataSet), }; _fluentValidationResults = new Dictionary>() { ["ManyErrors"] = GetFluentValidationResults(ComparisonDataSet.ManyErrorsDataSet), ["HalfErrors"] = GetFluentValidationResults(ComparisonDataSet.HalfErrorsDataSet), ["NoErrors"] = GetFluentValidationResults(ComparisonDataSet.NoErrorsDataSet), }; IReadOnlyList GetValidotResults(IReadOnlyList models) => models.Select(m => validotValidator.Validate(m)).ToList(); IReadOnlyList GetFluentValidationResults(IReadOnlyList models) => models.Select(m => fluentValidationValidator.Validate(m)).ToList(); } [Benchmark] public string ToString_FluentValidation() { var models = _fluentValidationResults[DataSet]; var t = ""; for(var i = 0; i < models.Count; ++i) { t = models[i].ToString(); } return t; } [Benchmark] public string ToString_Validot() { var models = _validotResults[DataSet]; var t = ""; for(var i = 0; i < models.Count; ++i) { t = models[i].ToString(); } return t; } } } ================================================ FILE: tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs ================================================ namespace Validot.Benchmarks.Comparisons { using System.Collections.Generic; using BenchmarkDotNet.Attributes; using FluentValidation; [MemoryDiagnoser] public class ValidationBenchmark { private Validot.IValidator _validotValidator; private ComparisonDataSet.FullModelValidator _fluentValidationValidator; private IReadOnlyDictionary> _dataSets; [Params("ManyErrors", "HalfErrors", "NoErrors")] public string DataSet { get; set; } [GlobalSetup] public void GlobalSetup() { _fluentValidationValidator = new ComparisonDataSet.FullModelValidator(); _validotValidator = Validator.Factory.Create(ComparisonDataSet.FullModelSpecification); _dataSets = ComparisonDataSet.DataSets; } [Benchmark] public bool IsValid_FluentValidation() { _fluentValidationValidator.CascadeMode = CascadeMode.Stop; var models = _dataSets[DataSet]; var t = true; for (var i = 0; i < models.Count; ++i) { t = _fluentValidationValidator.Validate(models[i]).IsValid; } return t; } [Benchmark] public bool IsValid_Validot() { var models = _dataSets[DataSet]; var t = true; for (var i = 0; i < models.Count; ++i) { t = _validotValidator.IsValid(models[i]); } return t; } [Benchmark] public object FailFast_FluentValidation() { _fluentValidationValidator.CascadeMode = CascadeMode.Stop; var models = _dataSets[DataSet]; object t = null; for (var i = 0; i < models.Count; ++i) { t = _fluentValidationValidator.Validate(models[i]); } return t; } [Benchmark] public object FailFast_Validot() { var models = _dataSets[DataSet]; var t = new object(); for (var i = 0; i < models.Count; ++i) { t = _validotValidator.Validate(models[i], true); } return t; } [Benchmark] public object Validate_FluentValidation() { var models = _dataSets[DataSet]; var t = new object(); for(var i = 0; i < models.Count; ++i) { t = _fluentValidationValidator.Validate(models[i]); } return t; } [Benchmark] public object Validate_Validot() { var models = _dataSets[DataSet]; var t = new object(); for(var i = 0; i < models.Count; ++i) { t = _validotValidator.Validate(models[i]); } return t; } } } ================================================ FILE: tests/Validot.Benchmarks/EmailModesBenchmark.cs ================================================ namespace Validot.Benchmarks { using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Attributes; using Bogus; using Validot.Results; [MemoryDiagnoser] public class EmailModesBenchmark { private IValidator _complexRegexEmailValidator; private IValidator _dataAnnotationsCompatibleEmailValidator; private IReadOnlyList _emails; [GlobalSetup] public void GlobalSetup() { _complexRegexEmailValidator = Validator.Factory.Create(e => e.Email(EmailValidationMode.ComplexRegex)); _dataAnnotationsCompatibleEmailValidator = Validator.Factory.Create(e => e.Email(EmailValidationMode.DataAnnotationsCompatible)); _emails = new Faker().RuleFor(m => m.Email, m => m.Person.Email).GenerateLazy(1_000_000).Select(m => m.Email).ToList(); } [Benchmark] public object ComplexRegex() { IValidationResult t = null; for(var i = 0; i < _emails.Count; ++i) { t = _complexRegexEmailValidator.Validate(_emails[i]); } return t; } [Benchmark] public object DataAnnotationsCompatible() { IValidationResult t = null; for(var i = 0; i < _emails.Count; ++i) { t = _dataAnnotationsCompatibleEmailValidator.Validate(_emails[i]); } return t; } private class EmailHolder { public string Email { get; set; } } } } ================================================ FILE: tests/Validot.Benchmarks/Program.cs ================================================ namespace Validot.Benchmarks { using System; using System.Collections.Generic; using System.IO; using System.Linq; using BenchmarkDotNet.Running; using Bogus; using Validot.Benchmarks.Comparisons; class Program { static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); // static void Main() // { // ErrorMessagesDataTest(); // } // static void Main() // { // SetupStats(); // } static void SetupStats() { int N = 10000; Randomizer.Seed = new Random(666); var fluentValidationValidator = new ComparisonDataSet.FullModelValidator(); var validotValidator = Validator.Factory.Create(ComparisonDataSet.FullModelSpecification); var manyErrorsModels = ComparisonDataSet.FullModelManyErrorsFaker.GenerateLazy(N).ToList(); var halfErrorsModels = ComparisonDataSet.FullModelHalfErrorsFaker.GenerateLazy(N).ToList(); var noErrorsModels = ComparisonDataSet.FullModelNoErrorsFaker.GenerateLazy(N).ToList(); var manyErrorsInvalidV = manyErrorsModels.Count(m => !validotValidator.IsValid(m)); var manyErrorsInvalidFV = manyErrorsModels.Count(m => !fluentValidationValidator.Validate(m).IsValid); var halfErrorsInvalidV = halfErrorsModels.Count(m => !validotValidator.IsValid(m)); var halfErrorsInvalidFV = halfErrorsModels.Count(m => !fluentValidationValidator.Validate(m).IsValid); var noErrorsInvalidV = noErrorsModels.Count(m => !validotValidator.IsValid(m)); var noErrorsInvalidFV = noErrorsModels.Count(m => !fluentValidationValidator.Validate(m).IsValid); Console.WriteLine($"ManyErrors invalid: FV={manyErrorsInvalidFV} V={manyErrorsInvalidV}"); Console.WriteLine($"HalfErrors invalid: FV={halfErrorsInvalidFV} V={halfErrorsInvalidV}"); Console.WriteLine($"NoErrors invalid: FV={noErrorsInvalidFV} V={noErrorsInvalidV}"); } static void ErrorMessagesDataTest() { int N = 1000; Randomizer.Seed = new Random(666); var fluentValidationValidator = new ComparisonDataSet.FullModelValidator(); var validotValidator = Validator.Factory.Create(ComparisonDataSet.FullModelSpecification); var manyErrorsModels = ComparisonDataSet.FullModelManyErrorsFaker.GenerateLazy(N).ToList(); var halfErrorsModels = ComparisonDataSet.FullModelHalfErrorsFaker.GenerateLazy(N).ToList(); var noErrorsModels = ComparisonDataSet.FullModelNoErrorsFaker.GenerateLazy(N).ToList(); WriteResults(manyErrorsModels, "many"); WriteResults(halfErrorsModels, "half"); WriteResults(noErrorsModels, "no"); void WriteResults(IReadOnlyList fullModels, string name) { for(var i = 0; i < N; ++i) { var validotIsValid = validotValidator.IsValid(fullModels[i]); if (validotIsValid) { File.AppendAllText(GetFile($"{name}.validot.valid"), i + Environment.NewLine); } else { File.AppendAllText(GetFile($"{name}.validot.invalid"), Environment.NewLine + $"-{i}-" + Environment.NewLine + validotValidator.Validate(fullModels[i])); } var fluentValidationResult = fluentValidationValidator.Validate(fullModels[i]); if (fluentValidationResult.IsValid) { File.AppendAllText(GetFile($"{name}.fluent.valid"), i + Environment.NewLine); } else { File.AppendAllText(GetFile($"{name}.fluent.invalid"), Environment.NewLine + Environment.NewLine + $"-{i}-" + Environment.NewLine + fluentValidationResult); } } } string GetFile(string name) { return Path.Combine(Directory.GetCurrentDirectory(), $"{name}.{N}.txt"); } } } } ================================================ FILE: tests/Validot.Benchmarks/Validot.Benchmarks.csproj ================================================ Exe net8.0 ================================================ FILE: tests/Validot.MemoryLeak/.editorconfig ================================================ root = true ================================================ FILE: tests/Validot.MemoryLeak/Program.cs ================================================ using System; namespace Validot.MemoryLeak { using System.Collections; using System.Collections.Generic; using Bogus; class Program { static int Main(string[] args) { Randomizer.Seed = new Random(666); var validator = Validator.Factory.Create(StreamDataSet.Specification); var value = ""; var dotCounter = 0; foreach (var model in StreamDataSet.Faker.GenerateForever()) { if (!validator.IsValid(model)) { var result = validator.Validate(model); if (!result.AnyErrors) { continue; } value = result.Codes.Count.ToString() + result.Paths.Count.ToString() + result.CodeMap.Count.ToString() + result.MessageMap.Count.ToString() + result.TranslationNames.ToString() + result.ToString(); if ((++dotCounter % 1000) == 0) { Console.Write("."); } } value = null; } return value is null ? 0 : 1; } } } ================================================ FILE: tests/Validot.MemoryLeak/StreamDataSet.cs ================================================ namespace Validot.MemoryLeak { using System; using System.Collections.Generic; using System.Linq; using Bogus; using Bogus.Extensions; public static class StreamDataSet { static StreamDataSet() { Specification nestedModelSpecification = _ => _ .Member(m => m.Number1, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message N1")) .Member(m => m.Number2, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message N2")) .Member(m => m.SuperNumber1, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message S1")) .Member(m => m.SuperNumber2, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message S2")) .Member(m => m.Text1, m => m.Rule(v => v.Contains('a', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Nested Message T1")) .Member(m => m.Text2, m => m.Rule(v => v.Contains('b', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Nested Message T2")); Specification = _ => _ .Member(m => m.Text1, m => m.Rule(v => v.Contains('a', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T1")) .Member(m => m.Text2, m => m.Rule(v => v.Contains('b', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T2")) .Member(m => m.Text3, m => m.Rule(v => v.Contains('c', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T3")) .Member(m => m.Text4, m => m.Rule(v => v.Contains('d', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T4")) .Member(m => m.Text5, m => m.Rule(v => v.Contains('e', StringComparison.InvariantCultureIgnoreCase)).WithMessage("Message T5")) .Member(m => m.Number1, m2 => m2.Rule(v => v < 10).WithMessage("Message N1")) .Member(m => m.Number2, m2 => m2.Rule(v => v < 10).WithMessage("Message N2")) .Member(m => m.Number3, m2 => m2.Rule(v => v < 10).WithMessage("Message N3")) .Member(m => m.Number4, m2 => m2.Rule(v => v < 10).WithMessage("Message N4")) .Member(m => m.Number5, m2 => m2.Rule(v => v < 10).WithMessage("Message N5")) .Member(m => m.SuperNumber1, m2 => m2.Rule(v => v < 10).WithMessage("Message S1")) .Member(m => m.SuperNumber2, m2 => m2.Rule(v => v < 10).WithMessage("Message S2")) .Member(m => m.SuperNumber3, m2 => m2.Rule(v => v < 10).WithMessage("Message S3")) .Member(m => m.NestedModel1, nestedModelSpecification) .Member(m => m.NestedModel2, nestedModelSpecification) .Member(m => m.ModelCollection, m => m .MaxCollectionSize(10).WithMessage("No more than 10 items are allowed") .AsCollection(nestedModelSpecification)) .Member(m => m.StructCollection, m => m .MaxCollectionSize(10).WithMessage("No more than 10 items are allowed") .AsCollection(m1 => m1.Rule(m2 => m2 < 10).WithMessage("Message C"))); var nestedModelsHalfErrorsFaker = new Faker() .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Text1, m => string.Join(" ", m.Lorem.Words(20))) .RuleFor(m => m.Text2, m => string.Join("b", m.Lorem.Words(15))) .RuleFor(m => m.SuperNumber1, m => m.Random.Number(0, 10)) .RuleFor(m => m.SuperNumber2, m => m.Random.Number(0, 9).OrNull(m, 0.01f)); Faker = new Faker() .RuleFor(m => m.Text1, m => string.Join("a", m.Lorem.Words(10))) .RuleFor(m => m.Text2, m => string.Join("b", m.Lorem.Words(10))) .RuleFor(m => m.Text3, m => string.Join("c", m.Lorem.Words(10))) .RuleFor(m => m.Text4, m => string.Join("d", m.Lorem.Words(10))) .RuleFor(m => m.Text5, m => string.Join(" ", m.Lorem.Words(20))) .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number3, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number4, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number5, m => m.Random.Int(0, 10)) .RuleFor(m => m.SuperNumber1, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber2, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber3, m => m.Random.Decimal(0, 10).OrNull(m, 0.01f)) .RuleFor(m => m.NestedModel1, m => nestedModelsHalfErrorsFaker.Generate()) .RuleFor(m => m.NestedModel2, m => nestedModelsHalfErrorsFaker.Generate()) .RuleFor(m => m.ModelCollection, m => nestedModelsHalfErrorsFaker.GenerateBetween(0, 9).ToList()) .RuleFor(m => m.StructCollection, m => Enumerable.Range(1, m.Random.Int(1, 11)).Select(_ => m.Random.Number(0, 9)).ToList()); } public class FullModel { public string Text1 { get; set; } public string Text2 { get; set; } public string Text3 { get; set; } public string Text4 { get; set; } public string Text5 { get; set; } public int Number1 { get; set; } public int Number2 { get; set; } public int Number3 { get; set; } public int Number4 { get; set; } public int Number5 { get; set; } public decimal? SuperNumber1 { get; set; } public decimal? SuperNumber2 { get; set; } public decimal? SuperNumber3 { get; set; } public NestedModel NestedModel1 { get; set; } public NestedModel NestedModel2 { get; set; } public IReadOnlyList ModelCollection { get; set; } public IReadOnlyList StructCollection { get; set; } } public class NestedModel { public string Text1 { get; set; } public string Text2 { get; set; } public int Number1 { get; set; } public int Number2 { get; set; } public decimal? SuperNumber1 { get; set; } public decimal? SuperNumber2 { get; set; } } public static Specification Specification { get; private set; } public static Faker Faker { get; private set;} } } ================================================ FILE: tests/Validot.MemoryLeak/Validot.MemoryLeak.csproj ================================================ Exe net6.0 ================================================ FILE: tests/Validot.MemoryLeak/Validot.MemoryLeak.dockerfile ================================================ FROM mcr.microsoft.com/dotnet/sdk:5.0 WORKDIR /usr/app/validot COPY tests/Validot.MemoryLeak Validot.MemoryLeak COPY src/Validot Validot RUN find . -iname "bin" | xargs rm -rf RUN find . -iname "obj" | xargs rm -rf RUN dotnet remove Validot.MemoryLeak/Validot.MemoryLeak.csproj reference ../../src/Validot/Validot.csproj RUN dotnet add Validot.MemoryLeak/Validot.MemoryLeak.csproj reference Validot/Validot.csproj RUN dotnet clean Validot.MemoryLeak/Validot.MemoryLeak.csproj RUN dotnet clean Validot/Validot.csproj RUN dotnet build Validot.MemoryLeak/Validot.MemoryLeak.csproj ENTRYPOINT [ "dotnet", "run", "--project", "Validot.MemoryLeak/Validot.MemoryLeak.csproj" ] ================================================ FILE: tests/Validot.Tests.Functional/.editorconfig ================================================ # C# files [*.cs] csharp_style_prefer_pattern_matching = false # Severity settings dotnet_diagnostic.IDE0022.severity = none dotnet_diagnostic.IDE0039.severity = none dotnet_diagnostic.IDE0058.severity = none dotnet_diagnostic.CA1310.severity = none dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1847.severity = none dotnet_diagnostic.CA1852.severity = none dotnet_diagnostic.CA1859.severity = none dotnet_diagnostic.CA1860.severity = none dotnet_diagnostic.CA1861.severity = none dotnet_diagnostic.CA1866.severity = none dotnet_diagnostic.CA2237.severity = none dotnet_diagnostic.SA1122.severity = none dotnet_diagnostic.SA1402.severity = none dotnet_diagnostic.SA1649.severity = none ================================================ FILE: tests/Validot.Tests.Functional/ConcurrencyFuncTests.cs ================================================ namespace Validot.Tests.Functional { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bogus; using Bogus.Extensions; using FluentAssertions; using Validot.Results; using Xunit; public class ConcurrencyTests { private void ShouldHaveSameContent(IValidationResult result1, IValidationResult result2) { result1.Codes.Should().HaveCount(result2.Codes.Count); if (result1.Codes.Any()) { result1.Codes.Should().Contain(result2.Codes); result2.Codes.Should().Contain(result1.Codes); } if (result1.Paths.Any()) { result1.Paths.Should().HaveCount(result2.Paths.Count); result1.Paths.Should().Contain(result2.Paths); result2.Paths.Should().Contain(result1.Paths); } foreach (var path in result1.MessageMap.Keys) { result1.MessageMap[path].Should().HaveCount(result2.MessageMap[path].Count); result1.MessageMap[path].Should().Contain(result2.MessageMap[path]); result2.MessageMap[path].Should().Contain(result1.MessageMap[path]); } foreach (var path in result1.CodeMap.Keys) { result1.CodeMap[path].Should().HaveCount(result2.CodeMap[path].Count); result1.CodeMap[path].Should().Contain(result2.CodeMap[path]); result2.CodeMap[path].Should().Contain(result1.CodeMap[path]); } result1.TranslationNames.Should().HaveCount(result2.TranslationNames.Count); if (result1.TranslationNames.Any()) { result1.TranslationNames.Should().Contain(result2.TranslationNames); result2.TranslationNames.Should().Contain(result1.TranslationNames); } } public class FullModel { public string Text1 { get; set; } public string Text2 { get; set; } public string Text3 { get; set; } public string Text4 { get; set; } public string Text5 { get; set; } public int Number1 { get; set; } public int Number2 { get; set; } public int Number3 { get; set; } public int Number4 { get; set; } public int Number5 { get; set; } public decimal? SuperNumber1 { get; set; } public decimal? SuperNumber2 { get; set; } public decimal? SuperNumber3 { get; set; } public NestedModel NestedModel1 { get; set; } public NestedModel NestedModel2 { get; set; } public IReadOnlyList ModelCollection { get; set; } public IReadOnlyList StructCollection { get; set; } } public class NestedModel { public string Text1 { get; set; } public string Text2 { get; set; } public int Number1 { get; set; } public int Number2 { get; set; } public decimal? SuperNumber1 { get; set; } public decimal? SuperNumber2 { get; set; } } [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public async Task Should_HaveSameResults_When_RunInConcurrentEnvironment(int concurrencyLevel) { Randomizer.Seed = new Random(666); Specification nestedModelSpecification = _ => _ .Member(m => m.Number1, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message N1")) .Member(m => m.Number2, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message N2")) .Member(m => m.SuperNumber1, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message S1")) .Member(m => m.SuperNumber2, m2 => m2.Rule(v => v < 10).WithMessage("Nested Message S2")) .Member(m => m.Text1, m => m.Rule(v => v.Contains('a')).WithMessage("Nested Message T1")) .Member(m => m.Text2, m => m.Rule(v => v.Contains('b')).WithMessage("Nested Message T2")); Specification fullModelSpecification = _ => _ .Member(m => m.Text1, m => m.Rule(v => v.Contains('a')).WithMessage("Message T1")) .Member(m => m.Text2, m => m.Rule(v => v.Contains('b')).WithMessage("Message T2")) .Member(m => m.Text3, m => m.Rule(v => v.Contains('c')).WithMessage("Message T3")) .Member(m => m.Text4, m => m.Rule(v => v.Contains('d')).WithMessage("Message T4")) .Member(m => m.Text5, m => m.Rule(v => v.Contains('e')).WithMessage("Message T5")) .Member(m => m.Number1, m2 => m2.Rule(v => v < 10).WithMessage("Message N1")) .Member(m => m.Number2, m2 => m2.Rule(v => v < 10).WithMessage("Message N2")) .Member(m => m.Number3, m2 => m2.Rule(v => v < 10).WithMessage("Message N3")) .Member(m => m.Number4, m2 => m2.Rule(v => v < 10).WithMessage("Message N4")) .Member(m => m.Number5, m2 => m2.Rule(v => v < 10).WithMessage("Message N5")) .Member(m => m.SuperNumber1, m2 => m2.Rule(v => v < 10).WithMessage("Message S1")) .Member(m => m.SuperNumber2, m2 => m2.Rule(v => v < 10).WithMessage("Message S2")) .Member(m => m.SuperNumber3, m2 => m2.Rule(v => v < 10).WithMessage("Message S3")) .Member(m => m.NestedModel1, nestedModelSpecification) .Member(m => m.NestedModel2, nestedModelSpecification) .Member(m => m.ModelCollection, m => m .MaxCollectionSize(10).WithMessage("No more than 10 items are allowed") .AsCollection(nestedModelSpecification)) .Member(m => m.StructCollection, m => m .MaxCollectionSize(10).WithMessage("No more than 10 items are allowed") .AsCollection(m1 => m1.Rule(m2 => m2 < 10).WithMessage("Message C"))); var validator = Validator.Factory.Create(fullModelSpecification); var nestedModelsHalfErrorsFaker = new Faker() .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Text1, m => string.Join(" ", m.Lorem.Words(20)).ToLowerInvariant()) .RuleFor(m => m.Text2, m => string.Join("b", m.Lorem.Words(15)).ToLowerInvariant()) .RuleFor(m => m.SuperNumber1, m => m.Random.Number(0, 10)) .RuleFor(m => m.SuperNumber2, m => m.Random.Number(0, 9).OrNull(m, 0.01f)); var fullModelHalfErrorsFaker = new Faker() .RuleFor(m => m.Text1, m => string.Join("a", m.Lorem.Words(10)).ToLowerInvariant()) .RuleFor(m => m.Text2, m => string.Join("b", m.Lorem.Words(10)).ToLowerInvariant()) .RuleFor(m => m.Text3, m => string.Join("c", m.Lorem.Words(10)).ToLowerInvariant()) .RuleFor(m => m.Text4, m => string.Join("d", m.Lorem.Words(10)).ToLowerInvariant()) .RuleFor(m => m.Text5, m => string.Join(" ", m.Lorem.Words(20)).ToLowerInvariant()) .RuleFor(m => m.Number1, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number2, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number3, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number4, m => m.Random.Int(0, 9)) .RuleFor(m => m.Number5, m => m.Random.Int(0, 10)) .RuleFor(m => m.SuperNumber1, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber2, m => m.Random.Decimal(0, 9)) .RuleFor(m => m.SuperNumber3, m => m.Random.Decimal(0, 10).OrNull(m, 0.01f)) .RuleFor(m => m.NestedModel1, m => nestedModelsHalfErrorsFaker.Generate()) .RuleFor(m => m.NestedModel2, m => nestedModelsHalfErrorsFaker.Generate()) .RuleFor(m => m.ModelCollection, m => nestedModelsHalfErrorsFaker.GenerateBetween(0, 9).ToList()) .RuleFor(m => m.StructCollection, m => Enumerable.Range(1, m.Random.Int(1, 11)).Select(_ => m.Random.Number(0, 9)).ToList()); var size = 5000; var dataSets = new IReadOnlyList[concurrencyLevel]; var resultSets = new IReadOnlyList[concurrencyLevel]; for (var i = 0; i < concurrencyLevel; ++i) { dataSets[i] = fullModelHalfErrorsFaker.Generate(size).ToList(); var r = new IValidationResult[size]; for (var j = 0; j < size; ++j) { r[j] = validator.Validate(dataSets[i][j]); } resultSets[i] = r; } var tasks = new Task[concurrencyLevel]; for (var i = 0; i < concurrencyLevel; ++i) { var dataSet = dataSets[i]; var results = resultSets[i]; tasks[i] = Task.Run(() => RunTaskForDataSet(dataSet, results)); } await Task.WhenAll(tasks).ConfigureAwait(false); void RunTaskForDataSet(IReadOnlyList dataSet, IReadOnlyList results) { for (var j = 0; j < size; ++j) { var validationResult = validator.Validate(dataSet[j]); ShouldHaveSameContent(validationResult, results[j]); } } } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/CustomRulesFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using Validot.Specification; using Xunit; namespace Step1 { using FluentAssertions; using Validot.Testing; public static class MyCustomValidotRules { public static IRuleOut HasCharacter(this IRuleIn @this) { return @this.RuleTemplate( value => value.Length > 0, "Must have at least one character!" ); } } public class CustomRulesFuncTests { [Fact] public void CustomRule() { Specification specification = s => s .HasCharacter(); var validator = Validator.Factory.Create(specification); validator.Validate("test").AnyErrors.Should().BeFalse(); validator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must have at least one character!"); } } } namespace Step2 { using System.Linq; using FluentAssertions; using Validot.Testing; public static class MyCustomValidotRules { public static IRuleOut HasCharacter( this IRuleIn @this, char character, int count = 1) { return @this.RuleTemplate( value => value.Count(c => c == character) == count, "Must have character '{character}' in the amount of {count}", Arg.Text(nameof(character), character), Arg.Number(nameof(count), count) ); } } public class CustomRulesFuncTests { [Fact] public void CustomRule() { Specification specification = s => s .HasCharacter('t', 2); var validator = Validator.Factory.Create(specification); validator.Validate("test").AnyErrors.Should().BeFalse(); validator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must have character 't' in the amount of 2"); } } } namespace Step3 { using System.Linq; using FluentAssertions; using Validot.Testing; public static class MyCustomValidotRules { public static IRuleOut HasCharacter( this IRuleIn @this, char character, int count = 1) { return @this.RuleTemplate( value => value.Count(c => c == character) == count, "Text.HasCharacter", Arg.Text(nameof(character), character), Arg.Number(nameof(count), count) ); } } public class CustomRulesFuncTests { [Fact] public void CustomRule() { Specification specification = s => s .HasCharacter('t', 2); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "Text.HasCharacter", "Must have character '{character}' in the amount of {count}") .WithTranslation("Polish", "Text.HasCharacter", "Musi zawierać znak '{character}' w ilości {count|culture=pl-PL}") ); validator.Validate("test").AnyErrors.Should().BeFalse(); var result = validator.Validate(""); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must have character 't' in the amount of 2"); result.ToString(translationName: "Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Musi zawierać znak 't' w ilości 2"); } } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/ErrorOutputFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using FluentAssertions; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Xunit; public class ErrorOutputFuncTests { [Fact] public void ErrorMessages() { Specification yearSpecification = s => s .Rule(year => year > -300) .WithMessage("Minimum year is 300 B.C.") .WithExtraMessage("Ancient history date is invalid.") .Rule(year => year != 0) .WithMessage("The year 0 is invalid.") .WithExtraMessage("There is no such year as 0.") .Rule(year => year < 10000) .WithMessage("Maximum year is 10000 A.D."); var validator = Validator.Factory.Create(yearSpecification); var result = validator.Validate(-500); result.MessageMap[""][0].Should().Be("Minimum year is 300 B.C."); result.MessageMap[""][1].Should().Be("Ancient history date is invalid."); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Minimum year is 300 B.C.", "Ancient history date is invalid." ); } [Fact] public void ErrorMessages_Paths() { Specification yearSpecification = s => s .Rule(year => year > -300) .WithMessage("Minimum year is 300 B.C.") .WithExtraMessage("Ancient history date is invalid.") .Rule(year => year != 0) .WithMessage("The year 0 is invalid.") .WithExtraMessage("There is no such year as 0.") .Rule(year => year < 10000) .WithMessage("Maximum year is 10000 A.D."); Specification bookSpecification = s => s .Member(m => m.YearOfFirstAnnouncement, yearSpecification) .Member(m => m.YearOfPublication, m => m.AsNullable(yearSpecification)) .Rule(m => m.YearOfFirstAnnouncement <= m.YearOfPublication) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication must be after the year of first announcement"); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { YearOfFirstAnnouncement = 0, YearOfPublication = -100 }; var result = validator.Validate(book); result.MessageMap["YearOfFirstAnnouncement"][0].Should().Be("The year 0 is invalid."); result.MessageMap["YearOfFirstAnnouncement"][1].Should().Be("There is no such year as 0."); result.MessageMap[""][0].Should().Be("Year of publication must be after the year of first announcement"); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Year of publication must be after the year of first announcement", "YearOfFirstAnnouncement: The year 0 is invalid.", "YearOfFirstAnnouncement: There is no such year as 0."); } [Fact] public void ErrorCodes() { Specification yearSpecification = s => s .Rule(year => year > -300) .WithCode("MIN_YEAR") .Rule(year => year != 0) .WithCode("ZERO_YEAR") .WithExtraCode("INVALID_VALUE") .Rule(year => year < 10000) .WithPath("YearOfPublication") .WithCode("MAX_YEAR"); var validator = Validator.Factory.Create(yearSpecification); var result = validator.Validate(0); result.Codes.Should().Contain("ZERO_YEAR", "INVALID_VALUE"); result.CodeMap[""][0].Should().Be("ZERO_YEAR"); result.CodeMap[""][1].Should().Be("INVALID_VALUE"); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "ZERO_YEAR, INVALID_VALUE" ); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/FactoryFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using System; using System.Collections.Generic; using System.Linq; using AssemblyWithHolders; using FluentAssertions; using Validot.Factory; using Validot.Settings; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Xunit; public class FactoryFuncTests { [Fact] public void Specification() { Specification authorSpecification = s => s .Member(m => m.Email, m => m .Email() .And() .EndsWith("@gmail.com") .WithMessage("Only gmail accounts are accepted") ); Specification bookSpecification = s => s .Member(m => m.Title, m => m.NotEmpty().NotWhiteSpace()) .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); var book = new BookModel() { Title = " ", Authors = new[] { new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "john.doe@outlook.com" }, new AuthorModel() { Email = "inv@lidem@il" }, } }; var validator = Validator.Factory.Create(bookSpecification, s => s .WithTranslation("English", "Texts.Email", "This is not a valid email address!") ); validator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: Must not consist only of whitespace characters", "Authors.#1.Email: Only gmail accounts are accepted", "Authors.#2.Email: This is not a valid email address!", "Authors.#2.Email: Only gmail accounts are accepted" ); var validator2 = Validator.Factory.Create(bookSpecification, validator.Settings); validator2.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: Must not consist only of whitespace characters", "Authors.#1.Email: Only gmail accounts are accepted", "Authors.#2.Email: This is not a valid email address!", "Authors.#2.Email: Only gmail accounts are accepted" ); } [Fact] public void SpecificationHolder() { var validator = Validator.Factory.Create(new BookSpecificationHolder()); var book = new BookModel() { Title = " ", Authors = new[] { new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "john.doe@outlook.com" }, new AuthorModel() { Email = "inv@lidem@il" }, } }; validator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: Must not consist only of whitespace characters", "Authors.#1.Email: Only gmail accounts are accepted", "Authors.#2.Email: Must be a valid email address", "Authors.#2.Email: Only gmail accounts are accepted" ); } [Fact] public void SettingsHolder() { var validator = Validator.Factory.Create(new AuthorSpecificationHolder()); var author = new AuthorModel() { Name = "", Email = "john.doe@outlook.com", }; var result = validator.Validate(author); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Name must not be empty", "Email: Invalid email"); result.ToString("Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Imię nie może być puste", "Email: Nieprawidłowy email"); validator.Settings.Translations.Keys.Should().HaveCount(2); validator.Settings.Translations.Keys.Should().Contain(new[] { "English", "Polish" }); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); var validator2 = Validator.Factory.Create( new AuthorSpecificationHolder(), s => s .WithReferenceLoopProtectionDisabled() .WithTranslation("English", "Invalid email", "The email address is invalid") ); validator2.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Name must not be empty", "Email: The email address is invalid"); validator2.Settings.ReferenceLoopProtectionEnabled.Should().BeFalse(); } [Fact] public void SettingsHolderWithInlineModification() { var validator = Validator.Factory.Create( new AuthorSpecificationHolder(), s => s .WithReferenceLoopProtectionDisabled() .WithTranslation("English", "Invalid email", "The email address is invalid") ); var author = new AuthorModel() { Name = "", Email = "john.doe@outlook.com", }; validator.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Name must not be empty", "Email: The email address is invalid"); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeFalse(); } [Fact] public void ReusingSettings() { Specification authorSpecification = s => s .Member(m => m.Email, m => m.Email().EndsWith("@gmail.com")) .WithMessage("Invalid email") .And() .Member(m => m.Name, m => m.NotEmpty()) .WithMessage("Name.EmptyValue"); var validator1 = Validator.Factory.Create( authorSpecification, s => s .WithTranslation("English", "Invalid email", "The email address is invalid") .WithTranslation("English", "Name.EmptyValue", "Name must not be empty") ); var validator2 = Validator.Factory.Create(authorSpecification, validator1.Settings); var author = new AuthorModel() { Name = "", Email = "john.doe@outlook.com", }; validator1.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Name must not be empty", "Email: The email address is invalid"); validator2.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Name must not be empty", "Email: The email address is invalid"); validator1.Settings.Should().BeSameAs(validator2.Settings); } [Fact] public void FetchingHolders() { var assemblies = new[] { typeof(HolderOfIntSpecificationAndSettings).Assembly }; var holder = Validator.Factory.FetchHolders(assemblies).Single(h => h.HolderType == typeof(HolderOfIntSpecificationAndSettings)); var validator = (Validator)holder.CreateValidator(); validator.Validate(11).ToString(translationName: "BinaryEnglish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "The maximum value is 0b1010" ); } public class BookSpecificationHolder : ISpecificationHolder { public BookSpecificationHolder() { Specification titleSpecification = s => s .NotEmpty() .NotWhiteSpace(); Specification emailSpecification = s => s .Email() .EndsWith("@gmail.com").WithMessage("Only gmail accounts are accepted"); Specification authorSpecification = s => s .Member(m => m.Email, emailSpecification); Specification bookSpecification = s => s .Member(m => m.Title, titleSpecification) .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); Specification = bookSpecification; } public Specification Specification { get; } } public class AuthorSpecificationHolder : ISpecificationHolder, ISettingsHolder { public AuthorSpecificationHolder() { Specification emailSpecification = s => s .Email() .EndsWith("@gmail.com"); Specification authorSpecification = s => s .Member(m => m.Email, emailSpecification).WithMessage("Invalid email") .Member(m => m.Name, m => m.NotEmpty()).WithMessage("Name.EmptyValue"); Specification = authorSpecification; Settings = s => s .WithReferenceLoopProtection() .WithPolishTranslation() .WithTranslation(new Dictionary>() { ["English"] = new Dictionary() { ["Name.EmptyValue"] = "Name must not be empty" }, ["Polish"] = new Dictionary() { ["Invalid email"] = "Nieprawidłowy email", ["Name.EmptyValue"] = "Imię nie może być puste" } }); } public Specification Specification { get; } public Func Settings { get; } } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/FluentApiFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using FluentAssertions; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Xunit; public class FluentApiFuncTests { [Fact] public void Rule() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m.Rule(isAgeValid); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.IsValid(12); // true ageValidator.IsValid(20); // false ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Error"); } [Fact] public void Rule_WithMessage() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m.Rule(isAgeValid).WithMessage("The age is invalid"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The age is invalid"); } [Fact] public void Rule_Relation() { Specification bookSpecification = m => m .Rule(book => book.IsSelfPublished == (book.Publisher is null)).WithMessage("Book must have a publisher or be self-published."); var bookValidator = Validator.Factory.Create(bookSpecification); bookValidator.Validate(new BookModel() { IsSelfPublished = true, Publisher = new PublisherModel() }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Book must have a publisher or be self-published." ); bookValidator.Validate(new BookModel() { IsSelfPublished = true, Publisher = null }).AnyErrors.Should().BeFalse(); } [Fact] public void Rule_NullMembers() { Specification publisherSpecification = m => m .Rule(publisher => { if (publisher.Name.Contains(publisher.CompanyId)) { return false; } return true; }); var validator = Validator.Factory.Create(publisherSpecification); Action action = () => { validator.Validate(new PublisherModel()); }; action.Should().ThrowExactly(); } [Fact] public void Rule_ExceptionBubbledUp() { var verySpecialException = new VerySpecialException(); Specification bookSpecification = m => m.Rule(book => throw verySpecialException); var bookValidator = Validator.Factory.Create(bookSpecification); try { bookValidator.Validate(new BookModel()); } catch (VerySpecialException exception) { object.ReferenceEquals(exception, verySpecialException).Should().BeTrue(); } } [Fact] public void RuleTemplate_RuleTemplateVsRule() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification1 = m => m.Rule(isAgeValid).WithMessage("The age is invalid"); Specification ageSpecification2 = m => m.RuleTemplate(isAgeValid, "The age is invalid"); var ageValidator1 = Validator.Factory.Create(ageSpecification1); var ageValidator2 = Validator.Factory.Create(ageSpecification2); ageValidator1.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The age is invalid"); ageValidator2.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The age is invalid"); } [Fact] public void RuleTemplate_Args() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate(isAgeValid, "Age must be between {minAge} and {maxAge}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18)); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Age must be between 0 and 18" ); } [Fact] public void RuleTemplate_ArgsWithParameters() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Age must be between 0.00 and 18,00" ); } [Fact] public void RuleTemplate_InvalidPlaceholders() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maximumAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Age must be between 0.00 and {maximumAge|format=0.00|culture=pl-PL}" ); } [Fact] public void RuleTemplate_MultipleParameters() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ) .WithExtraMessage("Must be more than {minAge}") .WithExtraMessage("Must be below {maxAge|format=0.00}! {maxAge}!"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Age must be between 0.00 and 18,00", "Must be more than 0", "Must be below 18.00! 18!" ); } [Fact] public void RuleTemplate_WithMessageUsingArgs() { Predicate isAgeValid = age => (age >= 0) && (age < 18); Specification ageSpecification = m => m .RuleTemplate( isAgeValid, "Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18) ) .WithMessage("Only {minAge}-{maxAge}!"); var ageValidator = Validator.Factory.Create(ageSpecification); ageValidator.Validate(32).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Only 0-18!" ); } [Fact] public void Member() { Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); var nameValidator = Validator.Factory.Create(nameSpecification); nameValidator.Validate("Adam !!!").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must consist of letters only!", "Must not contain whitespace!" ); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisher = new PublisherModel() { Name = "Adam !!!" }; publisherValidator.Validate(publisher).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Must consist of letters only!", "Name: Must not contain whitespace!" ); } [Fact] public void Member_NextLevel() { Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification); Specification bookSpecification = s => s .Member(m => m.Publisher, publisherSpecification); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Publisher = new PublisherModel() { Name = "Adam !!!" } }; bookValidator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Publisher.Name: Must consist of letters only!", "Publisher.Name: Must not contain whitespace!" ); } [Fact] public void Member_Inline() { Specification bookSpecification = s => s .Member(m => m.Publisher, m => m .Member(m1 => m1.Name, m1 => m1 .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ) ); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Publisher = new PublisherModel() { Name = "Adam !!!" } }; bookValidator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Publisher.Name: Must consist of letters only!", "Publisher.Name: Must not contain whitespace!" ); } [Fact] public void Member_InvalidMemberSelector() { Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); Specification bookSpecification = s => s .Member(m => m.Publisher.Name, nameSpecification); Action action = () => { _ = Validator.Factory.Create(bookSpecification); }; action.Should().ThrowExactly(); } [Fact] public void Member_HandlingNulls() { Specification publisherSpecification = s => s .Member(m => m.Name, m => m .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ); Specification publisherSpecificationRequired = s => s .Member(m => m.Name, m => m .Required().WithMessage("Must be filled in!") .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ); Specification publisherSpecificationOptional = s => s .Member(m => m.Name, m => m .Optional() .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!") ); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisherValidatorRequired = Validator.Factory.Create(publisherSpecificationRequired); var publisherValidatorOptional = Validator.Factory.Create(publisherSpecificationOptional); var publisher = new PublisherModel() { Name = null }; publisherValidator.Validate(publisher).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Required" ); publisherValidatorRequired.Validate(publisher).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Must be filled in!" ); publisherValidatorOptional.Validate(publisher).AnyErrors.Should().BeFalse(); } [Fact] public void AsModel() { Specification emailSpecification = s => s .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!"); Specification emailAsModelSpecification = s => s .AsModel(emailSpecification); var emailValidator = Validator.Factory.Create(emailSpecification); var emailAsModelValidator = Validator.Factory.Create(emailAsModelSpecification); emailValidator.Validate("invalid email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); emailAsModelValidator.Validate("invalid email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); } [Fact] public void AsModel_NestedLevels() { Specification emailSpecification = s => s .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!"); Specification emailNestedAsModelSpecification = s => s .AsModel(s1 => s1 .AsModel(s2 => s2 .AsModel(emailSpecification) ) ); var emailValidator = Validator.Factory.Create(emailSpecification); var emailAsModelValidator = Validator.Factory.Create(emailNestedAsModelSpecification); emailValidator.Validate("invalid email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); emailAsModelValidator.Validate("invalid email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); } [Fact] public void AsModel_MergingSpecifications() { Specification atRequiredSpecification = s => s .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!"); Specification allLettersLowerCaseSpecification = s => s .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!"); Specification lengthSpecification = s => s .Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters") .Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters"); Specification emailSpecification = s => s .AsModel(atRequiredSpecification) .AsModel(allLettersLowerCaseSpecification) .AsModel(lengthSpecification); var emailValidator = Validator.Factory.Create(emailSpecification); emailValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!", "All letters need to be lower case!", "Must be longer than 5 characters" ); } [Fact] public void AsModel_ChangingPresenceRule_ToOptional() { Specification atRequiredSpecification = s => s .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!"); Specification allLettersLowerCaseSpecification = s => s .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!"); Specification emailSpecification = s => s .Optional() .AsModel(atRequiredSpecification) .AsModel(allLettersLowerCaseSpecification) .Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters") .Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters"); var emailValidator = Validator.Factory.Create(emailSpecification); emailValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!", "All letters need to be lower case!", "Must be longer than 5 characters" ); emailValidator.Validate(null).AnyErrors.Should().BeFalse(); } [Fact] public void AsModel_ChangingPresenceRule_ToRequired() { Specification emailOptionalSpecification = s => s .Optional() .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!"); Specification emailSpecification = s => s .AsModel(emailOptionalSpecification); var emailOptionalValidator = Validator.Factory.Create(emailOptionalSpecification); var emailValidator = Validator.Factory.Create(emailSpecification); emailOptionalValidator.Validate(null).AnyErrors.Should().BeFalse(); emailOptionalValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); emailValidator.Validate(null).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required" ); emailValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); } [Fact] public void AsModel_BundleRules() { Specification emailSpecification = s => s .Rule(text => text.Contains('@')).WithMessage("Must contain @ character!") .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!") .Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters") .Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters"); Specification emailWrapperSpecification = s => s .AsModel(emailSpecification).WithMessage("This value is invalid as email address"); var emailValidator = Validator.Factory.Create(emailSpecification); var emailWrapperValidator = Validator.Factory.Create(emailWrapperSpecification); emailValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!", "All letters need to be lower case!", "Must be longer than 5 characters" ); emailWrapperValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "This value is invalid as email address" ); } [Fact] public void AsModel_BundleRules_InlineAndWithoutMessages() { Specification emailSpecification = s => s .AsModel(s1 => s1 .Rule(text => text.Contains('@')) .Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))) .Rule(text => text.Length > 5) .Rule(text => text.Length < 20) ).WithMessage("This value is invalid as email address"); var emailValidator = Validator.Factory.Create(emailSpecification); emailValidator.Validate("Email").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "This value is invalid as email address"); } [Fact] public void AsCollection() { Specification evenNumberSpecification = s => s .Rule(number => (number % 2) == 0).WithMessage("Number must be even"); Specification specification = s => s .AsCollection(evenNumberSpecification); var validator = Validator.Factory.Create(specification); var numbers = new[] { 1, 2, 3, 4, 5 }; validator.Validate(numbers).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "#0: Number must be even", "#2: Number must be even", "#4: Number must be even" ); } private class NumberCollection : IEnumerable, IEnumerable { public IEnumerable Ints { get; set; } public IEnumerable Doubles { get; set; } IEnumerator IEnumerable.GetEnumerator() => Doubles.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Ints.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator(); } [Fact] public void AsCollection_CustomIEnumerable() { Specification evenNumberSpecification = s => s .Rule(number => (number % 2) == 0).WithMessage("Number must be even"); Specification smallDecimalSpecification = s => s .Rule(number => Math.Floor(number) < 0.5).WithMessage("Decimal part must be below 0.5"); Specification specification = s => s .Optional() .AsCollection(evenNumberSpecification) .AsCollection(smallDecimalSpecification); var validator = Validator.Factory.Create(specification); var numberCollection = new NumberCollection() { Ints = new[] { 1, 2, 3, 4, 5 }, Doubles = new[] { 0.1, 2.8, 3.3, 4.6, 5.9 } }; validator.Validate(numberCollection).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "#0: Number must be even", "#1: Decimal part must be below 0.5", "#2: Decimal part must be below 0.5", "#2: Number must be even", "#3: Decimal part must be below 0.5", "#4: Number must be even", "#4: Decimal part must be below 0.5" ); } [Fact] public void AsCollection_NullItem() { Specification authorSpecification = s => s .Member(m => m.Email, m => m .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!") ); Specification bookSpecification = s => s .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { null, new AuthorModel() { Email = "foo@bar" }, new AuthorModel() { Email = null }, null, new AuthorModel() { Email = "InvalidEmail" }, null, } }; bookValidator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Authors.#0: Required", "Authors.#2.Email: Required", "Authors.#3: Required", "Authors.#4.Email: Must contain @ character!", "Authors.#5: Required" ); } [Fact] public void AsCollection_NullItem_Optional() { Specification authorSpecification = s => s .Optional() .Member(m => m.Email, m => m .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!") ); Specification bookSpecification = s => s .Member(m => m.Authors, m => m.AsCollection(authorSpecification)); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { null, new AuthorModel() { Email = "foo@bar" }, new AuthorModel() { Email = null }, null, new AuthorModel() { Email = "InvalidEmail" }, null, } }; bookValidator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Authors.#2.Email: Required", "Authors.#4.Email: Must contain @ character!" ); } [Fact] public void AsCollection_CollectionAndItems() { Specification authorSpecification = s => s .Optional() .Member(m => m.Email, m => m .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!") ); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification) .Rule(authors => authors.Count() <= 5).WithMessage("Book can have max 5 authors.") ); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { null, new AuthorModel() { Email = "foo@bar" }, new AuthorModel() { Email = null }, null, new AuthorModel() { Email = "InvalidEmail" }, null, } }; bookValidator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Authors.#2.Email: Required", "Authors.#4.Email: Must contain @ character!", "Authors: Book can have max 5 authors." ); } [Fact] public void AsNullable() { Specification numberSpecification = s => s .Rule(number => number < 10).WithMessage("Number must be less than 10"); Specification nullableSpecification = s => s .AsNullable(numberSpecification); var validator = Validator.Factory.Create(nullableSpecification); validator.Validate(5).AnyErrors.Should().BeFalse(); validator.Validate(15).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number must be less than 10" ); validator.Validate(null).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required" ); } [Fact] public void AsNullable_Optional() { Specification numberSpecification = s => s .Rule(number => number < 10).WithMessage("Number must be less than 10"); Specification nullableSpecification = s => s .Optional() .AsNullable(numberSpecification); var validator = Validator.Factory.Create(nullableSpecification); validator.Validate(5).AnyErrors.Should().BeFalse(); validator.Validate(null).AnyErrors.Should().BeFalse(); validator.Validate(15).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number must be less than 10" ); } [Fact] public void AsNullable_BuiltInRules() { Specification numberSpecification = s => s.GreaterThan(0).LessThan(10); Specification nullableSpecification = s => s.GreaterThan(0).LessThan(10); var numberValidator = Validator.Factory.Create(numberSpecification); var nullableValidator = Validator.Factory.Create(nullableSpecification); numberValidator.Validate(5).AnyErrors.Should().BeFalse(); nullableValidator.Validate(5).AnyErrors.Should().BeFalse(); numberValidator.Validate(15).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must be less than 10" ); nullableValidator.Validate(15).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must be less than 10" ); } [Fact] public void AsNullable_ReuseSpecification() { Specification yearSpecification = s => s .Rule(year => year >= -3000).WithMessage("Minimum year is 3000 B.C.") .Rule(year => year <= 3000).WithMessage("Maximum year is 3000 A.D."); Specification bookSpecification = s => s .Member(m => m.YearOfFirstAnnouncement, yearSpecification) .Member(m => m.YearOfPublication, m => m .Optional() .AsNullable(yearSpecification) ); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { YearOfFirstAnnouncement = -4000, YearOfPublication = 4000 }; bookValidator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "YearOfFirstAnnouncement: Minimum year is 3000 B.C.", "YearOfPublication: Maximum year is 3000 A.D." ); } [Fact] public void AsConverted_SameType() { Specification nameSpecification = s => s .Rule(name => char.IsUpper(name.First())).WithMessage("Must start with a capital letter!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); Converter sanitizeName = firstName => firstName.Trim(); Specification nameValueSpecification = s => s .AsConverted(sanitizeName, nameSpecification); var nameValidator = Validator.Factory.Create(nameValueSpecification); nameValidator.Validate("Bartosz").AnyErrors.Should().BeFalse(); nameValidator.Validate(" Bartosz ").AnyErrors.Should().BeFalse(); nameValidator.Validate(" bartosz ").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must start with a capital letter!" ); nameValidator.Validate(" Bart osz ").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not contain whitespace!" ); } [Fact] public void AsConverted_DifferentTypeAndInline() { Specification authorSpecification = s => s .Member(m => m.Name, m => m.AsConverted( name => name.Length, nameLength => nameLength.Rule(l => l % 2 == 0).WithMessage("Characters amount must be even")) ); var nameValidator = Validator.Factory.Create(authorSpecification); var author = new AuthorModel() { Name = "Bartosz" }; nameValidator.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Characters amount must be even" ); } [Fact] public void AsConverted_RequiredForValueTypeInTemplate() { Specification specification1 = s => s .AsConverted( value => value.ToString(CultureInfo.InvariantCulture), c => c.MaxLength(10).WithMessage("Number must be max 5 digits length") ); Validator.Factory.Create(specification1).Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required", "Number must be max 5 digits length" ); Specification specification2 = s => s .AsConverted( value => value.ToString(CultureInfo.InvariantCulture), c => c.Optional().MaxLength(10).WithMessage("Number must be max 5 digits length") ); Validator.Factory.Create(specification2).Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number must be max 5 digits length" ); } private class Animal { public int AnimalId { get; set; } } private class Mammal : Animal { public int MammalId { get; set; } } private class Elephant : Mammal { public int ElephantId { get; set; } } [Fact] public void AsType_SpecificationOfParentInSpecificationOfChild() { Specification idSpecification = s => s.NonZero(); Specification animalSpecification = s => s .Member(m => m.AnimalId, idSpecification); Specification elephantSpecification = s => s .Member(m => m.ElephantId, idSpecification) .AsType(animalSpecification); var elephantValidator = Validator.Factory.Create(elephantSpecification); elephantValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 10 }).AnyErrors.Should().BeFalse(); elephantValidator.Validate(new Elephant() { ElephantId = 0, AnimalId = 10 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "ElephantId: Must not be zero" ); elephantValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 0 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "AnimalId: Must not be zero" ); } [Fact] public void AsType_SpecificationOfChildInSpecificationOfParent() { Specification idSpecification = s => s.NonZero(); Specification elephantSpecification = s => s .Member(m => m.ElephantId, idSpecification); Specification animalSpecification = s => s .Member(m => m.AnimalId, idSpecification) .AsType(elephantSpecification); var animalValidator = Validator.Factory.Create(animalSpecification); animalValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 10 }).AnyErrors.Should().BeFalse(); animalValidator.Validate(new Elephant() { ElephantId = 0, AnimalId = 10 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "ElephantId: Must not be zero" ); animalValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 0 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "AnimalId: Must not be zero" ); } [Fact] public void AsType_NonRelatedTypes() { Specification specification = s => s .AsType(new Specification(number => number.NonZero())) .AsType(new Specification(text => text.NotEmpty())); var validator = Validator.Factory.Create(specification); validator.Validate(12).AnyErrors.Should().BeFalse(); validator.Validate("test").AnyErrors.Should().BeFalse(); validator.Validate(0L).AnyErrors.Should().BeFalse(); validator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be zero" ); validator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be empty" ); } [Fact] public void AsType_SummingAllLevels() { Specification idSpecification = s => s.NonZero(); Specification animalSpecification = s => s .Member(m => m.AnimalId, idSpecification); Specification mammalSpecification = s => s .Member(m => m.MammalId, idSpecification) .And() .Member(m => m.AnimalId, idSpecification) .WithMessage("Something wrong with animal from mammal perspective") .And() .AsType(animalSpecification); Specification elephantSpecification = s => s .Member(m => m.ElephantId, idSpecification) .And() .Member(m => m.MammalId, idSpecification) .WithMessage("Something wrong with mammal from elephant perspective") .And() .Member(m => m.AnimalId, idSpecification) .WithMessage("Something wrong with animal from elephant perspective") .And() .AsType(mammalSpecification); var elephantValidator = Validator.Factory.Create(elephantSpecification); elephantValidator.Validate(new Elephant() { ElephantId = 10, MammalId = 10, AnimalId = 10 }).AnyErrors.Should().BeFalse(); elephantValidator.Validate(new Elephant() { ElephantId = 0, MammalId = 10, AnimalId = 10 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "ElephantId: Must not be zero" ); elephantValidator.Validate(new Elephant() { ElephantId = 10, MammalId = 0, AnimalId = 10 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "MammalId: Must not be zero", "MammalId: Something wrong with mammal from elephant perspective" ); elephantValidator.Validate(new Elephant() { ElephantId = 0, MammalId = 0, AnimalId = 0 }).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "ElephantId: Must not be zero", "MammalId: Must not be zero", "MammalId: Something wrong with mammal from elephant perspective", "AnimalId: Must not be zero", "AnimalId: Something wrong with animal from mammal perspective", "AnimalId: Something wrong with animal from elephant perspective" ); } [Fact] public void AsDictionary_Simple() { Specification intValueSpecification = s => s .Rule(p => p % 2 == 0).WithMessage("Value must be even"); Specification> specification = s => s .AsDictionary(intValueSpecification); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = 11, ["Two"] = 22, ["Three"] = 33, ["Four"] = 44, ["Five"] = 55 }; validator.Validate(dictionary).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "One: Value must be even", "Three: Value must be even", "Five: Value must be even" ); } [Fact] public void AsDictionary_KeyStringifier() { Specification> specification = s => s .AsDictionary( k => k.Rule(p => p % 2 == 0).WithMessage("Value must be even"), k => k.ToUpperInvariant() ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = 11, ["Two"] = 22, ["Three"] = 33, ["Four"] = 44, ["Five"] = 55 }; validator.Validate(dictionary).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "ONE: Value must be even", "THREE: Value must be even", "FIVE: Value must be even" ); } [Fact] public void AsDictionary_NullValues() { Specification> specification = s => s .AsDictionary(k => k .Rule(p => p.Length % 2 == 0).WithMessage("Value length must be even") ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = "11", ["Two"] = "22222", ["Three"] = null, ["Four"] = null, ["Five"] = "55" }; validator.Validate(dictionary).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Two: Value length must be even", "Three: Required", "Four: Required" ); } [Fact] public void AsDictionary_NullValues_Optional() { Specification> specification = s => s .AsDictionary(k => k .Optional() .Rule(p => p.Length % 2 == 0).WithMessage("Value length must be even") ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["One"] = "11", ["Two"] = "22222", ["Three"] = null, ["Four"] = null, ["Five"] = "55" }; validator.Validate(dictionary).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Two: Value length must be even" ); } [Fact] public void AsDictionary_Template() { Specification> specification = s => s .AsDictionary(k => k .Rule(p => p.Length % 2 == 0).WithMessage("Value length must be even") ); var validator = Validator.Factory.Create(specification); validator.Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required", "#: Required", "#: Value length must be even" ); } [Fact] public void AsDictionary_KeyNormalization() { Specification> specification = s => s .AsDictionary( k => k.Rule(p => p % 2 == 0).WithMessage("Value must be even"), k => k.ToLowerInvariant() ); var validator = Validator.Factory.Create(specification); var dictionary = new Dictionary() { ["OnE..."] = 11, ["ThR...eE"] = 33, ["<<<...FiVe..."] = 55, ["...SeVeN"] = 77, ["<<> { public SimpleDictionary(Dictionary items) { Items = items; } private IEnumerable> Items { get; } IEnumerator> IEnumerable>.GetEnumerator() => Items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator(); } [Fact] public void AsDictionary_CustomClass() { Specification valueSpecification = s => s .Rule(p => p % 2 == 0).WithMessage("Value must be even"); Func keyStringifier = key => { var keyString = ""; for (var i = 0; i < key; i++) { keyString += "X"; } return keyString; }; Specification specification = s => s .AsDictionary(valueSpecification, keyStringifier); var validator = Validator.Factory.Create(specification); var dictionary = new SimpleDictionary(new Dictionary() { [1] = 11, [2] = 22, [3] = 33, [4] = 44, [5] = 55 }); validator.Validate(dictionary).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "X: Value must be even", "XXX: Value must be even", "XXXXX: Value must be even" ); } private class DoubleDictionary : IEnumerable>, IEnumerable> { private readonly IEnumerable> _ints; private readonly IEnumerable> _strings; public DoubleDictionary(Dictionary ints, Dictionary strings) { _ints = ints; _strings = strings; } IEnumerator> IEnumerable>.GetEnumerator() => _ints.GetEnumerator(); IEnumerator> IEnumerable>.GetEnumerator() => _strings.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); } [Fact] public void AsDictionary_CustomClassDoubleDictionary() { Specification intSpecification = s => s .Rule(p => p % 2 == 0).WithMessage("Value must be even"); Func intKeyStringifier = key => { var keyString = ""; for (var i = 0; i < key; i++) { keyString += "X"; } return keyString; }; Specification stringSpecification = s => s .Rule(p => p.Length < 3).WithMessage("Value must be shorter than 3 characters"); Func stringKeyStringifier = key => key.ToUpperInvariant(); Specification specification = s => s .AsDictionary(intSpecification, intKeyStringifier) .AsDictionary(stringSpecification, stringKeyStringifier); var validator = Validator.Factory.Create(specification); var dictionary = new DoubleDictionary( new Dictionary() { [1] = 11, [2] = 22, [3] = 33, [4] = 44, [5] = 55 }, new Dictionary() { ["One"] = "11", ["Two"] = "222", ["Three"] = "33", ["Four"] = "444", ["Five"] = "555" }); validator.Validate(dictionary).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "X: Value must be even", "XXX: Value must be even", "XXXXX: Value must be even", "TWO: Value must be shorter than 3 characters", "FOUR: Value must be shorter than 3 characters", "FIVE: Value must be shorter than 3 characters" ); } [Fact] public void WithCondition() { Predicate isValidEmail = email => email.Substring(0, email.IndexOf('@')).All(char.IsLetterOrDigit); Specification emailSpecification = s => s .Rule(isValidEmail) .WithCondition(email => email.Contains('@')) .WithMessage("Email username must contain only letters and digits."); var validator = Validator.Factory.Create(emailSpecification); validator.Validate("John.Doe-at-gmail.com").AnyErrors.Should().BeFalse(); validator.Validate("John.Doe@gmail.com").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Email username must contain only letters and digits." ); } [Fact] public void WithCondition_PreVerification() { Predicate isAuthorAPublisher = book => { return book.Authors.Any(a => a.Name == book.Publisher.Name); }; Specification bookSpecification = s => s .Rule(isAuthorAPublisher) .WithCondition(book => book.IsSelfPublished && book.Authors?.Any() == true && book.Publisher?.Name != null ) .WithMessage("Self-published book must have author as a publisher."); var validator = Validator.Factory.Create(bookSpecification); // 1: Condition is met, but the rule fails: var bookModel1 = new BookModel() { IsSelfPublished = true, Authors = new[] { new AuthorModel() { Name = "Bart" } }, Publisher = new PublisherModel() { Name = "Adam" } }; // 2: Condition is met, and the rule doesn't fail: var bookModel2 = new BookModel() { IsSelfPublished = true, Authors = new[] { new AuthorModel() { Name = "Bart" } }, Publisher = new PublisherModel() { Name = "Bart" } }; // 3: Condition is not met: var bookModel3 = new BookModel() { IsSelfPublished = false, Authors = new[] { new AuthorModel() { Name = "Bart" } }, Publisher = null }; validator.Validate(bookModel1).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Self-published book must have author as a publisher." ); validator.Validate(bookModel2).AnyErrors.Should().BeFalse(); validator.Validate(bookModel3).AnyErrors.Should().BeFalse(); } [Fact] public void WithCondition_SelectiveSpecification() { Specification gmailSpecification = s => s .Rule(email => { var username = email.Substring(0, email.Length - "@gmail.com".Length); return !username.Contains('.'); }).WithMessage("Gmail username must not contain dots."); Specification outlookSpecification = s => s .Rule(email => { var username = email.Substring(0, email.Length - "@outlook.com".Length); return username.All(char.IsLower); }).WithMessage("Outlook username must be all lower case."); Specification emailSpecification = s => s .Rule(email => email.Contains('@')).WithMessage("Must contain @ character!"); Predicate hasGmailAddress = a => a.Email?.EndsWith("@gmail.com") == true; Predicate hasOutlookAddress = a => a.Email?.EndsWith("@outlook.com") == true; Specification authorSpecification = s => s .Member(m => m.Email, gmailSpecification).WithCondition(hasGmailAddress) .Member(m => m.Email, outlookSpecification).WithCondition(hasOutlookAddress) .Member(m => m.Email, emailSpecification) .WithCondition(author => !hasGmailAddress(author) && !hasOutlookAddress(author)); var validator = Validator.Factory.Create(authorSpecification); var outlookAuthor = new AuthorModel() { Email = "John.Doe@outlook.com" }; var gmailAuthor = new AuthorModel() { Email = "John.Doe@gmail.com" }; var author1 = new AuthorModel() { Email = "JohnDoe" }; var author2 = new AuthorModel() { Email = "John.Doe@yahoo.com" }; validator.Validate(outlookAuthor).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Email: Outlook username must be all lower case."); validator.Validate(gmailAuthor).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Email: Gmail username must not contain dots."); validator.Validate(author1).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Email: Must contain @ character!"); validator.Validate(author2).AnyErrors.Should().BeFalse(); } [Fact] public void WithPath() { Specification specification1 = s => s .Rule(email => email.Contains('@')) .WithMessage("Must contain @ character!"); Specification specification2 = s => s .Rule(email => email.Contains('@')) .WithPath("Characters") .WithMessage("Must contain @ character!"); var validator1 = Validator.Factory.Create(specification1); var validator2 = Validator.Factory.Create(specification2); validator1.Validate("invalidemail").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character!" ); validator2.Validate("invalidemail").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Characters: Must contain @ character!" ); } [Fact] public void WithPath_GoingUp() { Specification bookSpecification = s => s .Member(m => m.Publisher, m => m .Member(m1 => m1.Name, m1 => m1 .Rule(name => name.All(char.IsLetter)) .WithPath("< specification = s => s .Rule(email => email.Contains('@')) .WithPath("Characters.") .WithMessage("Must contain @ character!"); Action action = () => { _ = Validator.Factory.Create(specification); }; action.Should().ThrowExactly(); } [Fact] public void WithPath_Member() { Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!"); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification).WithPath("FirstName"); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisher = new PublisherModel() { Name = "Adam !!!" }; publisherValidator.Validate(publisher).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "FirstName: Must consist of letters only!", "FirstName: Must not contain whitespace!" ); } [Fact] public void WithPath_MergeErrorOutputs() { Specification nameSpecification = s => s .Rule(name => name.All(char.IsLetter)).WithMessage("Name must consist of letters only!") .Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Name must not contain whitespace!"); Specification companyIdSpecification = s => s .Rule(name => name.Any()).WithMessage("Company Id must not be empty!"); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification).WithPath(" m.CompanyId, companyIdSpecification).WithPath(" nameSpecification = s => s .Rule(name => name.All(char.IsLetter)) .WithPath("Characters") .WithMessage("Must consist of letters only!") .Rule(name => char.IsUpper(name.First())) .WithPath("Grammar") .WithMessage("First letter must be capital!"); Specification publisherSpecification = s => s .Member(m => m.Name, nameSpecification); var publisherValidator = Validator.Factory.Create(publisherSpecification); var publisher = new PublisherModel() { Name = "adam !!!", }; publisherValidator.Validate(publisher).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name.Characters: Must consist of letters only!", "Name.Grammar: First letter must be capital!" ); } [Fact] public void WithMessage() { Specification specification = s => s .Rule(year => year != 0); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Error" ); Specification specificationWithMessage = s => s .Rule(year => year != 0) .WithMessage("Year 0 is invalid"); var validatorWithMessage = Validator.Factory.Create(specificationWithMessage); validatorWithMessage.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Year 0 is invalid" ); } [Fact] public void WithMessage_Args() { Specification specification = s => s .Between(min: 10, max: 20) .WithMessage("Minimum value is {min}. Maximum value is {max}"); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Minimum value is 10. Maximum value is 20" ); } [Fact] public void WithMessage_OverrideMultipleMessages() { Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithMessage("Contains author with invalid email") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; validator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Authors: Contains author with invalid email" ); } [Fact] public void WithExtraMessage() { Specification specification = s => s .Rule(year => year != 0) .WithMessage("Year 0 is invalid") .WithExtraMessage("Year 0 didn't exist") .WithExtraMessage("Please change to 1 B.C. or 1 A.D."); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Year 0 is invalid", "Year 0 didn't exist", "Please change to 1 B.C. or 1 A.D." ); } [Fact] public void WithExtraMessage_AddToEntireErrorOutput() { Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithExtraMessage("Contains author with invalid email") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; validator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Authors.#0.Email: Must be a valid email address", "Authors.#1.Email: Must be a valid email address", "Authors.#3.Email: Must be a valid email address", "Authors: Contains author with invalid email" ); } [Fact] public void WithExtraMessage_Args() { Specification specification = s => s .Between(min: 10, max: 20) .WithExtraMessage("Minimum value is {min}. Maximum value is {max}."); var validator = Validator.Factory.Create(specification); validator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must be between 10 and 20 (exclusive)", "Minimum value is 10. Maximum value is 20." ); } [Fact] public void WithCode() { Specification specification = s => s .Rule(year => year != 0) .WithCode("YEAR_ZERO"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(0); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "YEAR_ZERO" ); } [Fact] public void WithCode_Results() { Specification specification = s => s .AsCollection(m => m .Rule(year => year % 2 == 0).WithCode("IS_EVEN") .Rule(year => year % 2 != 0).WithCode("IS_ODD") ); var validator = Validator.Factory.Create(specification); var result = validator.Validate(new[] { 0, 1, 2, 3, 4 }); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "IS_EVEN, IS_ODD" ); result.Codes.Should().Contain("IS_EVEN", "IS_ODD"); result.CodeMap["#0"].Should().ContainSingle("IS_EVEN"); result.CodeMap["#1"].Should().ContainSingle("IS_ODD"); result.CodeMap["#2"].Should().ContainSingle("IS_EVEN"); result.CodeMap["#3"].Should().ContainSingle("IS_ODD"); result.CodeMap["#4"].Should().ContainSingle("IS_EVEN"); } [Fact] public void WithCode_OverrideEntireErrorOutput() { Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithCode("INVALID_AUTHORS") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; var result = validator.Validate(book); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "INVALID_AUTHORS" ); } [Fact] public void WithExtraCode() { Specification specification = s => s .Rule(year => year != 0) .WithCode("YEAR_ZERO") .WithExtraCode("INVALID_YEAR"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(0); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "YEAR_ZERO, INVALID_YEAR" ); } [Fact] public void WithExtraCode_Mix() { Specification authorSpecification = s => s.Member(m => m.Email, m => m.Email()); Specification bookSpecification = s => s .Member(m => m.Authors, m => m .AsCollection(authorSpecification).WithExtraCode("INVALID_AUTHORS") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Authors = new[] { new AuthorModel() { Email = "InvalidEmail1" }, new AuthorModel() { Email = "InvalidEmail2" }, new AuthorModel() { Email = "john.doe@gmail.com" }, new AuthorModel() { Email = "InvalidEmail3" }, } }; var result = validator.Validate(book); result.CodeMap["Authors"].Should().ContainSingle("INVALID_AUTHORS"); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "INVALID_AUTHORS", "", "Authors.#0.Email: Must be a valid email address", "Authors.#1.Email: Must be a valid email address", "Authors.#3.Email: Must be a valid email address" ); } [Fact] public void Optional() { Specification specification1 = s => s .Optional() .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator1 = Validator.Factory.Create(specification1); validator1.Validate(null).AnyErrors.Should().BeFalse(); Specification specification2 = s => s .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator2 = Validator.Factory.Create(specification2); var result2 = validator2.Validate(null); result2.AnyErrors.Should().BeTrue(); result2.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required"); validator1.Validate("a").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The minimum length is 3" ); validator2.Validate("a").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The minimum length is 3" ); validator1.Validate("abc").AnyErrors.Should().BeTrue(); validator2.Validate("abc").AnyErrors.Should().BeTrue(); } [Fact] public void Optional_Member() { Specification bookSpecification = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 3).WithMessage("The minimum length is 3") ); var validator = Validator.Factory.Create(bookSpecification); var book1 = new BookModel() { Title = null }; validator.Validate(book1).AnyErrors.Should().BeFalse(); var book2 = new BookModel() { Title = "a" }; validator.Validate(book2).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: The minimum length is 3" ); } [Fact] public void Required() { Specification specification1 = s => s .Required() .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator1 = Validator.Factory.Create(specification1); var result1 = validator1.Validate(null); result1.AnyErrors.Should().BeTrue(); result1.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required"); Specification specification2 = s => s .Rule(title => title.Length > 3) .WithMessage("The minimum length is 3"); var validator2 = Validator.Factory.Create(specification2); var result2 = validator2.Validate(null); result2.AnyErrors.Should().BeTrue(); result2.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required"); validator1.Validate("a").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The minimum length is 3" ); validator2.Validate("a").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The minimum length is 3" ); validator1.Validate("abc").AnyErrors.Should().BeTrue(); validator2.Validate("abc").AnyErrors.Should().BeTrue(); } [Fact] public void Required_WithMessageAndCodes() { Specification bookSpecification = s => s .Member(m => m.Title, m => m .Required().WithMessage("Title is required").WithExtraCode("MISSING_TITLE") .Rule(title => title.Length > 3).WithMessage("The minimum length is 3") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = null }; var result = validator.Validate(book); result.Codes.Should().ContainSingle("MISSING_TITLE"); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "MISSING_TITLE", "", "Title: Title is required" ); } [Fact] public void Forbidden() { Specification specification = s => s .Forbidden(); var validator = Validator.Factory.Create(specification); validator.Validate(null).AnyErrors.Should().BeFalse(); validator.Validate("some value").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Forbidden"); } [Fact] public void Forbidden_WithMessageAndCodes() { Specification bookSpecification = s => s .Member(m => m.Title, m => m .Forbidden().WithMessage("Title will be autogenerated").WithExtraCode("TITLE_EXISTS") ); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = "Aliens" }; var result = validator.Validate(book); result.Codes.Should().ContainSingle("TITLE_EXISTS"); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "TITLE_EXISTS", "", "Title: Title will be autogenerated" ); } [Fact] public void Required_WithPathWorkaround() { Specification bookSpecification = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 3).WithMessage("The minimum length is 3") ) .Rule(m => m.Title != null) .WithPath("BookTitle") .WithMessage("Title is required") .WithExtraCode("MISSING_TITLE"); var validator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = null }; var result = validator.Validate(book); result.Codes.Should().ContainSingle("MISSING_TITLE"); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "MISSING_TITLE", "", "BookTitle: Title is required" ); } [Fact] public void And_WithPathWorkaround() { Specification bookSpecificationPlain = s => s .Member(m => m.Title, m => m .Optional() .Rule(title => title.Length > 5).WithMessage("The minimum length is 5") .Rule(title => title.Length < 10).WithMessage("The maximum length is 10") ) .Rule(m => !m.Title.Contains("title")) .WithPath("Title") .WithCode("TITLE_IN_TITLE") .Rule(m => m.YearOfFirstAnnouncement < 3000) .WithMessage("Maximum year value is 3000"); Specification bookSpecificationAnd = s => s .Member(m => m.Title, m => m .Optional() .And() .Rule(title => title.Length > 5).WithMessage("The minimum length is 5") .And() .Rule(title => title.Length < 10).WithMessage("The maximum length is 10") ) .And() .Rule(m => !m.Title.Contains("title")) .WithPath("Title") .WithCode("TITLE_IN_TITLE") .And() .Rule(m => m.YearOfFirstAnnouncement < 3000) .WithMessage("Maximum year value is 3000"); var book = new BookModel() { Title = "Super long title", YearOfFirstAnnouncement = 3001 }; var resultPlain = Validator.Factory.Create(bookSpecificationPlain).Validate(book); resultPlain.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "TITLE_IN_TITLE", "", "Title: The maximum length is 10", "Maximum year value is 3000" ); var resultAnd = Validator.Factory.Create(bookSpecificationAnd).Validate(book); resultAnd.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "TITLE_IN_TITLE", "", "Title: The maximum length is 10", "Maximum year value is 3000" ); resultPlain.ToString().Should().Be(resultAnd.ToString()); } public class VerySpecialException : Exception { } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/MessageArgumentsFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using System; using Validot.Rules; using Validot.Testing; using Xunit; public class MessageArgumentsFuncTests { [Fact] public void MessageArguments() { Specification specification = s => s .Between(min: 0.123M, max: 100.123M) .WithMessage("The number needs to fit between {min} and {max}"); var validator = Validator.Factory.Create(specification); validator.Validate(105).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The number needs to fit between 0.123 and 100.123"); } [Fact] public void MessageArguments_Arguments() { Specification specification = s => s .Between(min: 0.123M, max: 100.123M) .WithMessage("The maximum value is {max|format=000.000}") .WithExtraMessage("The minimum value is {min|format=000.000|culture=pl-PL}"); var validator = Validator.Factory.Create(specification); validator.Validate(105).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The maximum value is 100.123", "The minimum value is 000,123"); } [Fact] public void EnumArgument() { Specification gmailSpecification = s => s .EndsWith("@gmail.com", stringComparison: StringComparison.OrdinalIgnoreCase) .WithMessage("Must ends with @gmail.com {stringComparison|translation=true}"); var validator = Validator.Factory.Create(gmailSpecification, settings => settings .WithTranslation("English", "Enum.System.StringComparison.OrdinalIgnoreCase", "(ignoring case!)") ); validator.Validate("john.doe@outlook.com").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must ends with @gmail.com (ignoring case!)"); } [Fact] public void GuidArgument() { Specification specification = s => s .NotEqualTo(new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")) .WithMessage("Must not be equal to: {value|format=X|case=upper}"); var validator = Validator.Factory.Create(specification); validator.Validate(new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be equal to: {0XC2CE1F3B,0X17E5,0X412E,{0X92,0X3B,0X6B,0X4E,0X26,0X8F,0X31,0XAA}}"); } [Fact] public void NumberArgument() { Specification specification = s => s .EqualTo(666.666M) .WithMessage("Needs to be equal to {value|format=0.0|culture=pl-PL}"); var validator = Validator.Factory.Create(specification); validator.Validate(10).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Needs to be equal to 666,7"); } [Fact] public void TextArgument() { Specification gmailSpecification = s => s .EndsWith("@gmail.com") .WithMessage("Must ends with {value|case=upper}"); var validator = Validator.Factory.Create(gmailSpecification); validator.Validate("john.doe@outlook.com").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must ends with @GMAIL.COM"); } [Fact] public void TimeArgument() { Specification specification = s => s .Before(new DateTime(2000, 1, 2, 3, 4, 5, 6)) .WithMessage("Must not be before: {max|format=yyyy MM dd + HH:mm}"); var validator = Validator.Factory.Create(specification); validator.Validate(new DateTime(2001, 1, 1, 1, 1, 1, 1)).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be before: 2000 01 02 + 03:04"); } [Fact] public void TranslationArgument() { Specification specification = s => s .NotEqualTo(666) .WithMessage("!!! {_translation|key=TripleSix} !!!"); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "TripleSix", "six six six") .WithTranslation("Polish", "TripleSix", "sześć sześć sześć") ); var result = validator.Validate(666); result.ToString(translationName: "English").ShouldResultToStringHaveLines( ToStringContentType.Messages, "!!! six six six !!!"); result.ToString(translationName: "Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "!!! sześć sześć sześć !!!"); } [Fact] public void PathArgument() { Specification specification = s => s .Positive() .WithPath("Number.Value") .WithMessage("Number value under {_path} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number.Value: Number value under Number.Value needs to be positive!"); } [Fact] public void PathArgument_Root() { Specification specification = s => s .Positive() .WithMessage("Number value under {_path} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number value under needs to be positive!"); } [Fact] public void NameArgument() { Specification specification = s => s .Positive() .WithPath("Number.Primary.SuperValue") .WithMessage("The {_name} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number.Primary.SuperValue: The SuperValue needs to be positive!"); } [Fact] public void NameArgument_Root() { Specification specification = s => s .Positive() .WithMessage("The {_name} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "The needs to be positive!"); } [Fact] public void NameArgument_TitleCase() { Specification specification = s => s .Positive() .WithPath("Number.Primary.SuperDuperValue123") .WithMessage("The {_name|format=titleCase} needs to be positive!"); var validator = Validator.Factory.Create(specification); var result = validator.Validate(-1); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Number.Primary.SuperDuperValue123: The Super Duper Value 123 needs to be positive!"); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/Models/A.cs ================================================ namespace Validot.Tests.Functional.Documentation.Models { public class A { public B B { get; set; } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/Models/AuthorModel.cs ================================================ namespace Validot.Tests.Functional.Documentation.Models { public class AuthorModel { public string Name { get; set; } public string Email { get; set; } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/Models/B.cs ================================================ namespace Validot.Tests.Functional.Documentation.Models { public class B { public A A { get; set; } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/Models/BookModel.cs ================================================ namespace Validot.Tests.Functional.Documentation.Models { using System.Collections.Generic; public class BookModel { public string Title { get; set; } public IEnumerable Authors { get; set; } public IEnumerable Languages { get; set; } public int YearOfFirstAnnouncement { get; set; } public int? YearOfPublication { get; set; } public PublisherModel Publisher { get; set; } public bool IsSelfPublished { get; set; } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/Models/Language.cs ================================================ namespace Validot.Tests.Functional.Documentation.Models { public enum Language { English, Polish } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/Models/PublisherModel.cs ================================================ namespace Validot.Tests.Functional.Documentation.Models { public class PublisherModel { public string CompanyId { get; set; } public string Name { get; set; } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/ParameterCommandsFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using Validot.Tests.Functional.Documentation.Models; using Xunit; public class ParameterCommandsFuncTests { [Fact] public void ParameterCommands() { Specification authorSpecification = s => s .Member(m => m.Name, m => m.NotWhiteSpace().MaxLength(100)) .WithCondition(m => !string.IsNullOrEmpty(m.Name)) .WithPath("AuthorName") .WithCode("AUTHOR_NAME_ERROR") .Member(m => m.Email, m => m.Email()) .WithMessage("Invalid email!") .WithExtraCode("EMAIL_ERROR") .Rule(m => m.Email != m.Name) .WithCondition(m => m.Email != null && m.Name != null) .WithPath("Email") .WithMessage("Name can't be same as Email"); _ = Validator.Factory.Create(authorSpecification); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/PresenceCommandsFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using Validot.Tests.Functional.Documentation.Models; using Xunit; public class PresenceCommandsFuncTests { [Fact] public void PresenceCommands() { Specification authorSpecification = s => s .Optional() .Member(m => m.Name, m => m .Optional() .NotWhiteSpace() .MaxLength(100) ) .Member(m => m.Email, m => m .Required().WithMessage("Email is obligatory.") .Email() ) .Rule(m => m.Email != m.Name); _ = Validator.Factory.Create(authorSpecification); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/ReferenceLoopFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using System; using FluentAssertions; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Validot.Validation.Stacks; using Xunit; public class ReferenceLoopFuncTests { [Fact] public void ReferenceLoop_Results() { Specification specificationB = null; Specification specificationA = s => s .Member(m => m.B, specificationB); specificationB = s => s .Member(m => m.A, specificationA); var validator = Validator.Factory.Create(specificationA); var a = new A() { B = new B() { A = new A() { B = new B() { A = null } } } }; validator.Validate(a).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "B.A.B.A: Required"); } [Fact] public void ReferenceLoop_Exception() { Specification specificationB = null; Specification specificationA = s => s .Member(m => m.B, specificationB); specificationB = s => s .Member(m => m.A, specificationA); var validator = Validator.Factory.Create(specificationA); var a = new A() { B = new B() { A = new A() { B = new B() { A = null } } } }; a.B.A.B.A = a.B.A; bool exceptionPresent = false; try { validator.Validate(a); } catch (ReferenceLoopException exception) { exception.Path.Should().Be("B.A"); exception.NestedPath.Should().Be("B.A.B.A"); exception.Type.Should().Be(typeof(A)); exceptionPresent = true; } exceptionPresent.Should().BeTrue(); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/ResultFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Xunit; public class ResultFuncTests { [Fact] public void AnyErrors() { Specification specification = s => s .NotEmpty(); var validator = Validator.Factory.Create(specification); var result1 = validator.Validate("test"); result1.AnyErrors.Should().BeFalse(); var result2 = validator.Validate(""); result2.AnyErrors.Should().BeTrue(); } [Fact] public void Paths() { Specification authorSpecification = s => s .Member(m => m.Email, m => m.Email().WithCode("EMAIL")) .Member(m => m.Name, m => m .NotEmpty() .MinLength(3) .NotContains("X").WithMessage("X character is not allowed in name") ); Specification bookSpecification = s => s .Member(m => m.Title, m => m.NotWhiteSpace()) .Member(m => m.Authors, m => m .AsCollection(authorSpecification) ) .Rule(m => m.IsSelfPublished == false).WithCode("ERROR_SELF_PUBLISHED"); var bookValidator = Validator.Factory.Create(bookSpecification); var book = new BookModel() { Title = "", Authors = new[] { new AuthorModel() { Email = "john.doe@gmail.com", Name = "X" }, new AuthorModel() { Email = "jane.doe@gmail.com", Name = "Jane" }, new AuthorModel() { Email = "inv@lidem@il", Name = "Jane" } }, IsSelfPublished = true }; var result = bookValidator.Validate(book); result.Paths.Should().HaveCount(4); result.Paths.Should().Contain(""); result.Paths.Should().Contain("Title"); result.Paths.Should().Contain("Authors.#0.Name"); result.Paths.Should().Contain("Authors.#2.Email"); } [Fact] public void Codes() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("NAME_ERROR") .MinLength(3).WithCode("SHORT_FIELD").WithExtraCode("NAME_ERROR") ) .Member(m => m.CompanyId, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("COMPANYID_ERROR") .NotContains("ID").WithCode("ID_IN_CONTENT") ) .Rule(m => m.Name != m.CompanyId).WithCode("SAME_VALUES"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "" }; var result = validator.Validate(publisher); result.Codes.Should().HaveCount(5); result.Codes.Should().Contain("EMPTY_FIELD", "NAME_ERROR", "SHORT_FIELD", "COMPANYID_ERROR", "SAME_VALUES"); } [Fact] public void CodeMap() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("NAME_ERROR") .MinLength(3).WithCode("SHORT_FIELD").WithExtraCode("NAME_ERROR") ) .Member(m => m.CompanyId, m => m .NotEmpty().WithCode("EMPTY_FIELD").WithExtraCode("COMPANYID_ERROR") .NotContains("company").WithCode("COPANY_IN_CONTENT") .NotContains("id").WithMessage("Invalid company value") ) .Rule(m => m.Name is null || m.CompanyId is null).WithCode("NULL_MEMBER"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "some_id" }; var result = validator.Validate(publisher); result.CodeMap["Name"].Should().Contain(new[] { "EMPTY_FIELD", "NAME_ERROR", "SHORT_FIELD", "NAME_ERROR" }); result.CodeMap["Name"].Where(n => n == "NAME_ERROR").Should().HaveCount(2); result.CodeMap[""].Should().ContainSingle("NULL_MEMBER"); result.Paths.Contains("CompanyId").Should().BeTrue(); result.CodeMap.Keys.Contains("CompanyId").Should().BeFalse(); result.MessageMap.Keys.Contains("CompanyId").Should().BeTrue(); } [Fact] public void MessageMap() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty().WithMessage("The field is empty").WithExtraMessage("Error in Name field") .MinLength(3).WithMessage("The field is too short").WithExtraMessage("Error in Name field") ) .Member(m => m.CompanyId, m => m .NotEmpty().WithMessage("The field is empty").WithExtraMessage("Error in CompanyId field") .NotContains("company").WithMessage("Company Id cannot contain 'company' word") .NotContains("id").WithCode("ID_IN_COMPANY") ) .Rule(m => m.Name is null || m.CompanyId is null) .WithMessage("All members must be present"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "some_id" }; var result = validator.Validate(publisher); result.MessageMap["Name"].Should().Contain(new[] { "The field is empty", "Error in Name field", "The field is too short", "Error in Name field" }); result.MessageMap["Name"].Where(n => n == "Error in Name field").Should().HaveCount(2); result.MessageMap[""].Should().ContainSingle("All members must be present"); result.Paths.Contains("CompanyId").Should().BeTrue(); result.MessageMap.Keys.Contains("CompanyId").Should().BeFalse(); result.CodeMap.Keys.Contains("CompanyId").Should().BeTrue(); } [Fact] public void GetTranslatedMessageMap() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .MinLength(3).WithMessage("Name is too short") ) .Member(m => m.Email, m => m .Email() ); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() .WithTranslation("Polish", "Name is too short", "Imię jest zbyt krótkie") ); var author = new AuthorModel() { Name = "", Email = "inv@lidem@il" }; var result = validator.Validate(author); var englishMessageMap = result.GetTranslatedMessageMap("English"); englishMessageMap["Name"].Should().Contain(new[] { "Must not be empty", "Name is too short" }); englishMessageMap["Email"].Should().ContainSingle("Must be a valid email address"); var polishMessageMap = result.GetTranslatedMessageMap("Polish"); polishMessageMap["Name"].Should().Contain(new[] { "Musi nie być puste", "Imię jest zbyt krótkie" }); polishMessageMap["Email"].Should().ContainSingle("Musi być poprawnym adresem email"); } [Fact] public void GetTranslatedMessageMap_InvalidTranslationName() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .MinLength(3).WithMessage("Name is too short") ) .Member(m => m.Email, m => m .Email() ); var author = new AuthorModel() { Name = "", Email = "inv@lidem@il" }; var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() .WithTranslation("Polish", "Name is too short", "Imię jest zbyt krótkie") ); var result = validator.Validate(author); Action action = () => { _ = result.GetTranslatedMessageMap("Russian"); }; action.Should().ThrowExactly(); } [Fact] public void TranslationNames_Default() { Specification specification = s => s.Rule(m => false); var model = new AuthorModel(); var validator = Validator.Factory.Create(specification); var result = validator.Validate(model); result.TranslationNames.Should().ContainSingle("English"); } [Fact] public void TranslationNames() { Specification specification = s => s.Rule(m => false); var model = new AuthorModel(); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() ); var result = validator.Validate(model); result.TranslationNames.Should().Contain(new[] { "Polish", "English" }); } [Fact] public void ToStringMethod() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .WithMessage("The field is empty") .WithExtraMessage("Error in Name field") .WithExtraCode("NAME_EMPTY") .MinLength(3) .WithMessage("The field is too short") .WithExtraCode("NAME_TOO_SHORT") ) .Member(m => m.CompanyId, m => m .NotEmpty() .NotContains("id") .WithCode("ID_IN_COMPANY") ) .Rule(m => m.Name is null || m.CompanyId is null) .WithMessage("All members must be present"); var validator = Validator.Factory.Create(specification); var publisher = new PublisherModel() { Name = "", CompanyId = "some_id" }; var result = validator.Validate(publisher); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "NAME_EMPTY, NAME_TOO_SHORT, ID_IN_COMPANY", "", "Name: The field is empty", "Name: Error in Name field", "Name: The field is too short", "All members must be present" ); } [Fact] public void ToStringMethod_Translation() { Specification specification = s => s .Member(m => m.Name, m => m .NotEmpty() .MinLength(3) ) .Member(m => m.CompanyId, m => m .NotEmpty().WithMessage("CompanyId field is required") ); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() .WithTranslation("Polish", "CompanyId field is required", "Pole CompanyId jest wymagane") ); var publisher = new PublisherModel() { Name = "", CompanyId = "" }; var result = validator.Validate(publisher); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Must not be empty", "Name: Must be at least 3 characters in length", "CompanyId: CompanyId field is required" ); result.ToString("Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Name: Musi nie być puste", "Name: Musi być długości minimalnie 3 znaków", "CompanyId: Pole CompanyId jest wymagane" ); Action action = () => { result.ToString("Russian"); }; action.Should().ThrowExactly(); } [Fact] public void ToStringMethod_Valid() { Specification specification = s => s; var validator = Validator.Factory.Create(specification); var model = new PublisherModel(); var result = validator.Validate(model); result.AnyErrors.Should().BeFalse(); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "OK" ); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/ScopeCommandsFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using Validot.Tests.Functional.Documentation.Models; using Xunit; public class ScopeCommandsFuncTests { [Fact] public void ScopeCommands() { Specification authorSpecification = s => s .Member(m => m.Name, m => m.NotWhiteSpace().MaxLength(100)) .Member(m => m.Email, m => m.Email()) .Rule(m => m.Email != m.Name); _ = Validator.Factory.Create(authorSpecification); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/SettingsFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using System.Collections.Generic; using FluentAssertions; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Xunit; public class SettingsFuncTests { [Fact] public void Settings_Factory() { Specification specification = s => s; var validator = Validator.Factory.Create(specification, settings => settings .WithReferenceLoopProtection() ); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); } [Fact] public void WithTranslation() { var validator = Validator.Factory.Create(s => s, settings => settings .WithTranslation("English", "Global.Error", "Error found") .WithTranslation("English", "Global.Required", "Value is required") .WithTranslation("Polish", "Global.Required", "Wartość wymagana") ); validator.Settings.Translations["English"]["Global.Error"].Should().Be("Error found"); validator.Settings.Translations["English"]["Global.Required"].Should().Be("Value is required"); validator.Settings.Translations["Polish"]["Global.Required"].Should().Be("Wartość wymagana"); } [Fact] public void WithTranslation_FullDictionary() { var validator = Validator.Factory.Create(s => s, settings => settings .WithTranslation(new Dictionary>() { ["English"] = new Dictionary() { ["Global.Error"] = "Error found", ["Global.Required"] = "Value is required", }, ["Polish"] = new Dictionary() { ["Global.Required"] = "Wartość wymagana", } }) ); validator.Settings.Translations["English"]["Global.Error"].Should().Be("Error found"); validator.Settings.Translations["English"]["Global.Required"].Should().Be("Value is required"); validator.Settings.Translations["Polish"]["Global.Required"].Should().Be("Wartość wymagana"); } [Fact] public void WithTranslation_Dictionaries() { var validator = Validator.Factory.Create(s => s, settings => settings .WithTranslation("English", new Dictionary() { ["Global.Error"] = "Error found", ["Global.Required"] = "Value is required", }) .WithTranslation("Polish", new Dictionary() { ["Global.Required"] = "Wartość wymagana", }) ); validator.Settings.Translations["English"]["Global.Error"].Should().Be("Error found"); validator.Settings.Translations["English"]["Global.Required"].Should().Be("Value is required"); validator.Settings.Translations["Polish"]["Global.Required"].Should().Be("Wartość wymagana"); } [Fact] public void WithTranslation_OverwriteDefaults() { Specification specification = s => s .Member(m => m.Email, m => m .NotEmpty() .Email() ) .Member(m => m.Name, m => m .Required().WithMessage("Name is required") ); var author = new AuthorModel() { Email = "" }; var validator1 = Validator.Factory.Create(specification); validator1.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Email: Must not be empty", "Email: Must be a valid email address", "Name: Name is required"); var validator2 = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "Name is required", "You must fill out the name") .WithTranslation("English", "Texts.NotEmpty", "Text value cannot be empty") ); validator2.Validate(author).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Email: Text value cannot be empty", "Email: Must be a valid email address", "Name: You must fill out the name"); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/SpecificationFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using Validot.Tests.Functional.Documentation.Models; using Xunit; public class SpecificationFuncTests { [Fact] public void ChainCommands() { Specification yearSpecification = s => s .GreaterThan(-10000) .NotEqualTo(0).WithMessage("There is no such year as 0") .LessThan(3000); _ = Validator.Factory.Create(yearSpecification); } [Fact] public void Scopes() { Specification yearSpecification = s => s .GreaterThan(-10000) .NotEqualTo(0).WithMessage("There is no such year as 0") .LessThan(3000); Specification bookSpecification = s => s .Member(m => m.YearOfFirstAnnouncement, yearSpecification) .Member(m => m.YearOfPublication, m => m .Positive() ) .Rule(m => m.YearOfPublication == m.YearOfFirstAnnouncement).WithMessage("Same year in both places is invalid"); _ = Validator.Factory.Create(bookSpecification); } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/TranslationsFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using Validot.Testing; using Xunit; public class TranslationsFuncTests { [Fact] public void Translation_PrintingMessageKey() { Specification specification = s => s .Rule(m => m.Contains("@")).WithMessage("Must contain @ character"); var validator = Validator.Factory.Create(specification); validator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain @ character"); } [Fact] public void Translation_DeliveringTranslation() { Specification specification = s => s .Rule(m => m.Contains("@")).WithMessage("Must contain @ character"); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("Polish", "Must contain @ character", "Musi zawierać znak: @") .WithTranslation("English", "Must contain @ character", "Must contain character: @") ); var result = validator.Validate(""); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain character: @"); result.ToString("Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Musi zawierać znak: @"); } [Fact] public void OverridingMessages() { Specification specification = s => s .NotEmpty(); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation("English", "Global.Required", "String cannot be null!") .WithTranslation("English", "Texts.NotEmpty", "String cannot be empty!") ); validator.Validate(null).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "String cannot be null!" ); validator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "String cannot be empty!" ); } [Fact] public void OverridingMessages_Arguments() { Specification specification = s => s .BetweenOrEqualTo(16.66M, 666.666M); var validator = Validator.Factory.Create(specification, settings => settings .WithTranslation( "English", "Numbers.BetweenOrEqualTo", "Only numbers between {min|format=000.0000} and {max|format=000.0000} are valid!") ); validator.Validate(10).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Only numbers between 016.6600 and 666.6660 are valid!" ); } public class BuiltInTranslationsTests { [Fact] public void WithPolishTranslation() { Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithPolishTranslation() ); validator.Validate(null).ToString(translationName: "Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Wymagane" ); validator.Validate("").ToString(translationName: "Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Musi nie być puste" ); validator.Validate("1234567890").ToString(translationName: "Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Musi być długości maksymalnie 5 znaków" ); } [Fact] public void WithSpanishTranslation() { Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithSpanishTranslation() ); validator.Validate(null).ToString(translationName: "Spanish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Requerido" ); validator.Validate("").ToString(translationName: "Spanish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "No debe estar vacío" ); validator.Validate("1234567890").ToString(translationName: "Spanish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Debe tener como máximo 5 caracteres" ); } [Fact] public void WithRussianTranslation() { Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithRussianTranslation() ); validator.Validate(null).ToString(translationName: "Russian").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Требуется" ); validator.Validate("").ToString(translationName: "Russian").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Не должен быть пуст" ); validator.Validate("1234567890").ToString(translationName: "Russian").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Должен быть не больше 5 символов в длину" ); } [Fact] public void WithPortugueseTranslation() { Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithPortugueseTranslation() ); validator.Validate(null).ToString(translationName: "Portuguese").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Obrigatório" ); validator.Validate("").ToString(translationName: "Portuguese").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Não deve estar vazio" ); validator.Validate("1234567890").ToString(translationName: "Portuguese").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Deve ter no máximo 5 caracteres" ); } [Fact] public void WithGermanTranslation() { Specification specification = s => s .NotEmpty() .MaxLength(5); var validator = Validator.Factory.Create(specification, settings => settings .WithGermanTranslation() ); validator.Validate(null).ToString(translationName: "German").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Erforderlich" ); validator.Validate("").ToString(translationName: "German").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Darf nicht leer sein" ); validator.Validate("1234567890").ToString(translationName: "German").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Muss maximal 5 Zeichen lang sein" ); } } } namespace CustomTranslation { using System.Collections.Generic; using Validot.Settings; public static class WithYodaEnglishExtension { public static ValidatorSettings WithYodaEnglish(this ValidatorSettings @this) { var dictionary = new Dictionary() { ["Global.Required"] = "Exist, it must.", ["Numbers.LessThan"] = "Greater than {max}, the number must, be not." }; return @this.WithTranslation("YodaEnglish", dictionary); } } public class CustomTranslationsFuncTests { [Fact] public void CustomTranslation_Extension() { Specification specification = s => s .LessThan(10); var validator = Validator.Factory.Create(specification, settings => settings .WithYodaEnglish() ); validator.Validate(null).ToString("YodaEnglish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Exist, it must." ); validator.Validate(20).ToString("YodaEnglish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "Greater than 10, the number must, be not." ); } } } } ================================================ FILE: tests/Validot.Tests.Functional/Documentation/ValidatorFuncTests.cs ================================================ namespace Validot.Tests.Functional.Documentation { using FluentAssertions; using Validot.Settings; using Validot.Testing; using Validot.Tests.Functional.Documentation.Models; using Xunit; public class ValidatorFuncTests { [Fact] public void Validator_Create() { Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()); var validator = Validator.Factory.Create(specification); _ = validator; } [Fact] public void Validator_CreateWithSettings() { Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()) .And() .Rule(m => m.YearOfPublication > m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue); var validator = Validator.Factory.Create( specification, s => s.WithPolishTranslation() ); _ = validator; } [Fact] public void Validator_ExecutionOptimization() { Specification specification = s => s .Member(m => m.Title, m => m .NotEmpty() .NotWhiteSpace() .NotEqualTo("blank") .And() .Rule(t => !t.StartsWith(" ")).WithMessage("Can't start with whitespace") ) .WithMessage("Contains errors!"); var validator = Validator.Factory.Create(specification); var book = new BookModel() { Title = " " }; validator.Validate(book).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: Contains errors!"); } [Fact] public void Validate() { Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()) .And() .Member(m => m.YearOfFirstAnnouncement, m => m.BetweenOrEqualTo(1000, 3000)) .And() .Rule(m => m.YearOfPublication >= m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication needs to be after the first announcement"); var validator = Validator.Factory.Create(specification); var book = new BookModel() { Title = "", YearOfPublication = 600, YearOfFirstAnnouncement = 666 }; var result = validator.Validate(book); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: Must not be empty", "YearOfFirstAnnouncement: Must be between 1000 and 3000 (inclusive)", "Year of publication needs to be after the first announcement"); var failFastResult = validator.Validate(book, failFast: true); failFastResult.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Title: Must not be empty"); } [Fact] public void Validate_IsValid() { Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()) .And() .Member(m => m.YearOfFirstAnnouncement, m => m.BetweenOrEqualTo(1000, 3000)) .And() .Rule(m => m.YearOfPublication >= m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication needs to be after the first announcement"); var validator = Validator.Factory.Create(specification); var book1 = new BookModel() { Title = "", YearOfPublication = 600, YearOfFirstAnnouncement = 666 }; validator.IsValid(book1).Should().BeFalse(); var book2 = new BookModel() { Title = "test", YearOfPublication = 1666, YearOfFirstAnnouncement = 1600 }; validator.IsValid(book2).Should().BeTrue(); } [Fact] public void Template() { Specification specification = s => s .NotEmpty() .NotWhiteSpace().WithMessage("White space is not allowed") .Rule(m => m.Contains("@")).WithMessage("Must contain @ character"); var validator = Validator.Factory.Create(specification); validator.Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required", "Must not be empty", "White space is not allowed", "Must contain @ character"); } [Fact] public void Template_AsCollection() { Specification specification = s => s .Member(m => m.Authors, m => m .AsCollection(m1 => m1 .Member(m2 => m2.Name, m2 => m2.NotEmpty()) ) ); var validator = Validator.Factory.Create(specification); validator.Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required", "Authors: Required", "Authors.#: Required", "Authors.#.Name: Required", "Authors.#.Name: Must not be empty"); } [Fact] public void Template_ReferenceLoop() { Specification specificationB = null; Specification specificationA = s => s .Member(m => m.B, specificationB); specificationB = s => s .Member(m => m.A, specificationA); var validator = Validator.Factory.Create(specificationA); validator.Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required", "B: Required", "B.A: (reference loop)"); var a = new A() { B = new B() }; a.B.A = a; } [Fact] public void Template_Complex() { Specification authorSpecification = s => s .Member(m => m.Email, m => m .NotWhiteSpace().WithMessage("Email cannot be whitespace") .Email() ) .Member(m => m.Name, m => m .NotEmpty() .NotWhiteSpace() .MinLength(2) ); Specification specification = s => s .Member(m => m.Title, m => m.NotEmpty()).WithExtraCode("EMPTY_TITLE") .Member(m => m.YearOfFirstAnnouncement, m => m.BetweenOrEqualTo(1000, 3000)) .Member(m => m.Authors, m => m .AsCollection(authorSpecification) .MaxCollectionSize(4).WithMessage("Book shouldn't have more than 4 authors").WithExtraCode("MANY_AUTHORS") ) .Rule(m => m.YearOfPublication >= m.YearOfFirstAnnouncement) .WithCondition(m => m.YearOfPublication.HasValue) .WithMessage("Year of publication needs to be after the first announcement"); var validator = Validator.Factory.Create(specification); validator.Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "EMPTY_TITLE, MANY_AUTHORS", "", "Required", "Year of publication needs to be after the first announcement", "Title: Required", "Title: Must not be empty", "YearOfFirstAnnouncement: Must be between 1000 and 3000 (inclusive)", "Authors: Required", "Authors: Book shouldn't have more than 4 authors", "Authors.#: Required", "Authors.#.Email: Required", "Authors.#.Email: Email cannot be whitespace", "Authors.#.Email: Must be a valid email address", "Authors.#.Name: Required", "Authors.#.Name: Must not be empty", "Authors.#.Name: Must not consist only of whitespace characters", "Authors.#.Name: Must be at least 2 characters in length"); } } } ================================================ FILE: tests/Validot.Tests.Functional/Readme/FeaturesFuncTests.cs ================================================ namespace Validot.Tests.Functional.Readme { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Specification; using Validot.Testing; using Xunit; public static class RulesExtensions { public static IRuleOut ExactLinesCount(this IRuleIn @this, int count) { return @this.RuleTemplate( value => value.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Length == count, "Must contain exactly {count} lines", Arg.Number("count", count) ); } } public class FeaturesFuncTests { public class UserModel { public string Name { get; set; } public string LastName { get; set; } public string PrimaryEmail { get; set; } public IEnumerable AlternativeEmails { get; set; } } public class BookModel { public string Title { get; set; } public string AuthorEmail { get; set; } public decimal Price { get; set; } } [Fact] public void FluentApi() { Specification nameSpecification = s => s .LengthBetween(5, 50) .SingleLine() .Rule(name => name.All(char.IsLetterOrDigit)); Specification emailSpecification = s => s .Email() .And() .Rule(email => email.All(char.IsLower)) .WithMessage("Must contain only lower case characters"); Specification userSpecification = s => s .Member(m => m.Name, nameSpecification) .WithMessage("Must comply with name rules") .And() .Member(m => m.PrimaryEmail, emailSpecification) .And() .Member(m => m.AlternativeEmails, m => m .Optional() .And() .MaxCollectionSize(3) .WithMessage("Must not contain more than 3 addresses") .And() .AsCollection(emailSpecification) ) .And() .Rule(user => { return user.PrimaryEmail is null || user.AlternativeEmails?.Contains(user.PrimaryEmail) == false; }) .WithMessage("Alternative emails must not contain the primary email address"); _ = Validator.Factory.Create(userSpecification); } [Fact] public void Validators() { Specification bookSpecification = s => s .Optional() .Member(m => m.AuthorEmail, m => m.Optional().Email()) .Member(m => m.Title, m => m.NotEmpty().LengthBetween(1, 100)) .Member(m => m.Price, m => m.NonNegative()); var bookValidator = Validator.Factory.Create(bookSpecification); var bookModel = new BookModel() { AuthorEmail = "inv@lid_em@il", Price = 10 }; bookValidator.IsValid(bookModel).Should().BeFalse(); bookValidator.Validate(bookModel).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "AuthorEmail: Must be a valid email address", "Title: Required" ); bookValidator.Validate(bookModel, failFast: true).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "AuthorEmail: Must be a valid email address" ); bookValidator.Template.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "AuthorEmail: Must be a valid email address", "Title: Required", "Title: Must not be empty", "Title: Must be between 1 and 100 characters in length", "Price: Must not be negative" ); } [Fact] public void Rules() { Specification specification1 = s => s .ExactLinesCount(4); Validator.Factory.Create(specification1).Validate(string.Empty).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must contain exactly 4 lines"); Specification specification2 = s => s .ExactLinesCount(4).WithMessage("Required lines count: {count}"); Validator.Factory.Create(specification2).Validate(string.Empty).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required lines count: 4"); Specification specification3 = s => s .ExactLinesCount(4).WithMessage("Required lines count: {count|format=000.00|culture=pl-PL}"); Validator.Factory.Create(specification3).Validate(string.Empty).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required lines count: 004,00"); } [Fact] public void Translations() { Specification specification = s => s .Member(m => m.PrimaryEmail, m => m.Email()) .Member(m => m.Name, m => m.LengthBetween(3, 50)); var validator = Validator.Factory.Create(specification, settings => settings.WithPolishTranslation()); var model = new UserModel() { PrimaryEmail = "in@lid_em@il", Name = "X" }; var result = validator.Validate(model); result.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "PrimaryEmail: Must be a valid email address", "Name: Must be between 3 and 50 characters in length" ); result.ToString(translationName: "Polish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "PrimaryEmail: Musi być poprawnym adresem email", "Name: Musi być długości pomiędzy 3 a 50 znaków" ); } [Fact] public void HandlingNulls() { Specification specification1 = s => s .Member(m => m.LastName, m => m .Rule(lastName => lastName.Length < 50) .Rule(lastName => lastName.All(char.IsLetter)) ); var validationResult1 = Validator.Factory.Create(specification1).Validate(null); validationResult1.AnyErrors.Should().BeTrue(); validationResult1.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Required" ); Specification specification2 = s => s .Optional() .Member(m => m.LastName, m => m .Rule(lastName => lastName.Length < 50) .Rule(lastName => lastName.All(char.IsLetter)) ); var validationResult2 = Validator.Factory.Create(specification2).Validate(null); validationResult2.AnyErrors.Should().BeFalse(); } } } ================================================ FILE: tests/Validot.Tests.Functional/Readme/QuickStartFuncTests.cs ================================================ namespace Validot.Tests.Functional.Readme { using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot; using Validot.Testing; using Xunit; public class QuickStartTest { public class UserModel { public UserModel(string email = null, string name = null, int age = 0) { Email = email; Name = name; Age = age; } public string Email { get; set; } public string Name { get; set; } public int Age { get; set; } public Dictionary Dict { get; set; } } [Fact] public void QuickStart() { Specification specification = _ => _ .Member(m => m.Email, m => m .Email() .WithExtraCode("ERR_EMAIL") .And() .MaxLength(100) ) .Member(m => m.Name, m => m .Optional() .And() .LengthBetween(8, 100) .And() .Rule(name => name.All(char.IsLetterOrDigit)) .WithMessage("Must contain only letter or digits") ) .And() .Rule(m => m.Age >= 18 || m.Name != null) .WithPath("Name") .WithMessage("Required for underaged user") .WithExtraCode("ERR_NAME"); var validator = Validator.Factory.Create(specification); var model = new UserModel(email: "inv@lidv@lue", age: 14); var result = validator.Validate(model); result.AnyErrors.Should().BeTrue(); result.MessageMap["Email"].Single().Should().Be("Must be a valid email address"); result.Codes.Should().Contain("ERR_EMAIL", "ERR_NAME"); var messagesString = result.ToString(); messagesString.ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "ERR_EMAIL, ERR_NAME", "", "Email: Must be a valid email address", "Name: Required for underaged user"); } } } ================================================ FILE: tests/Validot.Tests.Functional/Validot.Tests.Functional.csproj ================================================ net8.0 false runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: tests/Validot.Tests.Unit/.editorconfig ================================================ [*.cs] # ReSharper/Rider inspection properties resharper_object_creation_as_statement_highlighting = silent resharper_redundant_explicit_array_creation_highlighting = silent resharper_assignment_is_fully_discarded_highlighting = silent resharper_possible_multiple_enumeration_highlighting = silent resharper_redundant_type_arguments_of_method_highlighting = suggestion resharper_redundant_empty_object_or_collection_initializer_highlighting = silent resharper_unused_auto_property_saccessor_local_highlighting = suggestion resharper_unused_auto_property_accessor_global_highlighting = suggestion resharper_co_variant_array_conversion_highlighting = suggestion resharper_redundant_extends_list_entry_highlighting = suggestion # Severity settings dotnet_diagnostic.IDE0017.severity = none dotnet_diagnostic.IDE0022.severity = none dotnet_diagnostic.IDE0028.severity = none dotnet_diagnostic.IDE0039.severity = none dotnet_diagnostic.IDE0050.severity = suggestion dotnet_diagnostic.IDE0052.severity = suggestion dotnet_diagnostic.IDE0058.severity = none dotnet_diagnostic.IDE0062.severity = none dotnet_diagnostic.IDE1005.severity = none dotnet_diagnostic.CA1034.severity = none dotnet_diagnostic.CA1051.severity = none dotnet_diagnostic.CA1052.severity = none dotnet_diagnostic.CA1062.severity = none dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1711.severity = none dotnet_diagnostic.CA1812.severity = none dotnet_diagnostic.CA1806.severity = none dotnet_diagnostic.CA1815.severity = none dotnet_diagnostic.CA1819.severity = none dotnet_diagnostic.CA1820.severity = none dotnet_diagnostic.CA1823.severity = suggestion dotnet_diagnostic.CA1825.severity = none dotnet_diagnostic.CA1845.severity = none dotnet_diagnostic.CA1847.severity = none dotnet_diagnostic.CA1852.severity = none dotnet_diagnostic.CA1859.severity = none dotnet_diagnostic.CA1860.severity = none dotnet_diagnostic.CA1861.severity = none dotnet_diagnostic.SA1118.severity = none dotnet_diagnostic.SA1122.severity = none dotnet_diagnostic.SA1125.severity = none dotnet_diagnostic.SA1129.severity = none dotnet_diagnostic.SA1139.severity = none dotnet_diagnostic.SA1201.severity = none dotnet_diagnostic.SA1204.severity = none dotnet_diagnostic.SA1401.severity = none dotnet_diagnostic.SA1600.severity = none dotnet_diagnostic.SA1601.severity = none dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.NS1005.severity = none dotnet_diagnostic.SYSLIB1045.severity = none ================================================ FILE: tests/Validot.Tests.Unit/CodeHelperTests.cs ================================================ namespace Validot.Tests.Unit { using System.Collections.Generic; using FluentAssertions; using Xunit; public class CodeHelperTests { public static IEnumerable Codes_Invalid() { yield return new object[] { "code code" }; yield return new object[] { "code " }; yield return new object[] { " code" }; } public static IEnumerable Codes_Valid() { yield return new object[] { "#ABC" }; yield return new object[] { "_ABC_" }; yield return new object[] { "code" }; yield return new object[] { "code1_code2" }; yield return new object[] { "CODE_!@#_123" }; yield return new object[] { "CODE," }; yield return new object[] { ",CODE" }; } [Theory] [MemberData(nameof(Codes_Valid))] public void IsCodeValid_Should_ReturnTrue_When_CodeIsValid(string code) { CodeHelper.IsCodeValid(code).Should().BeTrue(); } [Theory] [MemberData(nameof(Codes_Invalid))] public void IsCodeValid_Should_ReturnFalse_When_CodeIsInvalid(string code) { CodeHelper.IsCodeValid(code).Should().BeFalse(); } } } ================================================ FILE: tests/Validot.Tests.Unit/ErrorContentApiHelper.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors; using Validot.Specification; using Validot.Validation.Scopes.Builders; public static class ErrorContentApiHelper { public class ExpectedErrorContent { public IReadOnlyList Messages { get; set; } = Array.Empty(); public IReadOnlyList Codes { get; set; } = Array.Empty(); internal ErrorMode Mode { get; set; } = ErrorMode.Append; public bool ShouldBeEmpty(int initialMessagesAmount = 1, int initialCodesAmount = 0) { var emptyMessages = (Mode == ErrorMode.Override || initialMessagesAmount == 0) && Messages.Count == 0; var emptyCodes = (Mode == ErrorMode.Override || initialCodesAmount == 0) && Codes.Count == 0; return emptyMessages && emptyCodes; } public void Match(IError error, int initialMessagesAmount = 1, int initialCodesAmount = 0) { var messagesStartIndex = Mode == ErrorMode.Override ? 0 : initialMessagesAmount; var codesStartIndex = Mode == ErrorMode.Override ? 0 : initialCodesAmount; error.Messages.Count.Should().Be(messagesStartIndex + Messages.Count); error.Codes.Count.Should().Be(codesStartIndex + Codes.Count); for (var i = 0; i < Messages.Count; ++i) { error.Messages[i + messagesStartIndex].Should().Be(Messages[i]); } for (var i = 0; i < Codes.Count; ++i) { error.Codes[i + codesStartIndex].Should().Be(Codes[i]); } } } public static IEnumerable AllCases() { var list = new List>() { SettingsOnlyMessages(), SettingsOnlyCodes(), SettingMessagesAndCodes() }; foreach (var item in list) { foreach (var i in item) { yield return i; } } } public static IEnumerable NoCommand() { yield return new object[] { "0", new Func>(target => { return target; }), new ExpectedErrorContent() }; } public static IEnumerable SettingsOnlyMessages() { yield return new object[] { "M1", new Func>(target => { target = WithMessage(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "123" } } }; yield return new object[] { "M2", new Func>(target => { target = WithMessage(target, "123"); target = WithExtraMessage(target, "456"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "123", "456" } } }; yield return new object[] { "M3", new Func>(target => { target = WithExtraMessage(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Messages = new[] { "123", } } }; yield return new object[] { "M4", new Func>(target => { target = WithMessage(target, "123"); target = WithExtraMessage(target, "456"); target = WithExtraMessage(target, "789"); target = WithExtraMessage(target, "101112"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "123", "456", "789", "101112" } } }; yield return new object[] { "M4", new Func>(target => { target = WithExtraMessage(target, "123"); target = WithExtraMessage(target, "456"); target = WithExtraMessage(target, "789"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Messages = new[] { "123", "456", "789", } } }; yield return new object[] { "M5", new Func>(target => { target = WithMessage(target, "123"); target = WithExtraMessage(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "123", "123" } } }; yield return new object[] { "M6", new Func>(target => { target = WithExtraMessage(target, "123"); target = WithExtraMessage(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Messages = new[] { "123", "123" } } }; yield return new object[] { "M7", new Func>(target => { target = WithMessage(target, "123"); target = WithExtraMessage(target, "456"); target = WithExtraMessage(target, "456"); target = WithExtraMessage(target, "789"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "123", "456", "456", "789", } } }; } public static IEnumerable SettingsOnlyCodes() { yield return new object[] { "C1", new Func>(target => { target = WithCode(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Codes = new[] { "123" } } }; yield return new object[] { "C2", new Func>(target => { target = WithCode(target, "123"); target = WithExtraCode(target, "456"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Codes = new[] { "123", "456" } } }; yield return new object[] { "C3", new Func>(target => { target = WithExtraCode(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Codes = new[] { "123", } } }; yield return new object[] { "C4", new Func>(target => { target = WithCode(target, "123"); target = WithExtraCode(target, "456"); target = WithExtraCode(target, "789"); target = WithExtraCode(target, "101112"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Codes = new[] { "123", "456", "789", "101112" } } }; yield return new object[] { "C4", new Func>(target => { target = WithExtraCode(target, "123"); target = WithExtraCode(target, "456"); target = WithExtraCode(target, "789"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Codes = new[] { "123", "456", "789", } } }; yield return new object[] { "C5", new Func>(target => { target = WithCode(target, "123"); target = WithExtraCode(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Codes = new[] { "123", "123" } } }; yield return new object[] { "C6", new Func>(target => { target = WithExtraCode(target, "123"); target = WithExtraCode(target, "123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Codes = new[] { "123", "123" } } }; yield return new object[] { "C7", new Func>(target => { target = WithCode(target, "123"); target = WithExtraCode(target, "456"); target = WithExtraCode(target, "456"); target = WithExtraCode(target, "789"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Codes = new[] { "123", "456", "456", "789", } } }; } public static IEnumerable SettingMessagesAndCodes() { yield return new object[] { "MC1", new Func>(target => { target = WithMessage(target, "m123"); target = WithExtraCode(target, "c123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "m123" }, Codes = new[] { "c123" }, } }; yield return new object[] { "MC2", new Func>(target => { target = WithMessage(target, "m123"); target = WithExtraMessage(target, "m456"); target = WithExtraCode(target, "c123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Override, Messages = new[] { "m123", "m456" }, Codes = new[] { "c123", }, } }; yield return new object[] { "MC3", new Func>(target => { target = WithExtraMessage(target, "m123"); target = WithExtraCode(target, "c123"); return target; }), new ExpectedErrorContent() { Mode = ErrorMode.Append, Messages = new[] { "m123", }, Codes = new[] { "c123", }, } }; } private static dynamic WithMessage(dynamic api, string message) { if (api is IWithMessageIn withMessageIn) { return WithMessageExtension.WithMessage(withMessageIn, message); } if (api is IWithMessageForbiddenIn withMessageForbiddenIn) { return WithMessageExtension.WithMessage(withMessageForbiddenIn, message); } throw new InvalidOperationException("Dynamic api tests failed"); } private static dynamic WithExtraMessage(dynamic api, string message) { if (api is IWithExtraMessageIn withExtraMessageIn) { return WithExtraMessageExtension.WithExtraMessage(withExtraMessageIn, message); } if (api is IWithExtraMessageForbiddenIn withExtraMessageForbiddenIn) { return WithExtraMessageExtension.WithExtraMessage(withExtraMessageForbiddenIn, message); } throw new InvalidOperationException("Dynamic api tests failed"); } private static dynamic WithCode(dynamic api, string code) { if (api is IWithCodeIn withCodeIn) { return WithCodeExtension.WithCode(withCodeIn, code); } if (api is IWithCodeForbiddenIn withCodeForbiddenIn) { return WithCodeExtension.WithCode(withCodeForbiddenIn, code); } throw new InvalidOperationException("Dynamic api tests failed"); } private static dynamic WithExtraCode(dynamic api, string code) { if (api is IWithExtraCodeIn withExtraCodeIn) { return WithExtraCodeExtension.WithExtraCode(withExtraCodeIn, code); } if (api is IWithExtraCodeForbiddenIn withExtraCodeForbiddenIn) { return WithExtraCodeExtension.WithExtraCode(withExtraCodeForbiddenIn, code); } throw new InvalidOperationException("Dynamic api tests failed"); } } } ================================================ FILE: tests/Validot.Tests.Unit/ErrorSetupApiHelper.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using Validot.Specification; public static class ErrorSetupApiHelper { public class ExpectedErrorSetup { public Predicate ShouldExecute { get; set; } public string Path { get; set; } } public static IEnumerable AllCases() { yield return new object[] { "S0", new Func>(target => { return target; }), new ExpectedErrorSetup() { Path = null } }; yield return new object[] { "S1", new Func>(target => { target = WithPath(target, "name123"); return target; }), new ExpectedErrorSetup() { Path = "name123" } }; Predicate predicate = x => true; yield return new object[] { "S2", new Func>(target => { target = WithCondition(target, predicate); return target; }), new ExpectedErrorSetup() { ShouldExecute = predicate } }; yield return new object[] { "S3", new Func>(target => { target = WithCondition(target, predicate); target = WithPath(target, "name123"); return target; }), new ExpectedErrorSetup() { ShouldExecute = predicate, Path = "name123" } }; } private static dynamic WithPath(dynamic api, string message) { if (api is IWithPathIn withPathIn) { return WithPathExtension.WithPath(withPathIn, message); } throw new InvalidOperationException("Dynamic api tests failed"); } private static dynamic WithCondition(dynamic api, Predicate predicate) { if (api is IWithConditionIn withConditionIn) { return WithConditionExtension.WithCondition(withConditionIn, predicate); } throw new InvalidOperationException("Dynamic api tests failed"); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/ArgHelperTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class ArgHelperTests { public class FormatMessage { [Fact] public void Should_ReturnEmptyString_When_MessageIsNull() { var formattedMessage = ArgHelper.FormatMessage(null, Array.Empty(), Array.Empty()); formattedMessage.Should().Be(string.Empty); } [Fact] public void Should_ReturnMessage_When_NullPlaceholders() { var formattedMessage = ArgHelper.FormatMessage("test {test}", null, new[] { Arg.Text("test", "XXX") }); formattedMessage.Should().Be("test {test}"); } [Fact] public void Should_ReturnMessage_When_NoPlaceholders() { var formattedMessage = ArgHelper.FormatMessage("test {test}", Array.Empty(), new[] { Arg.Text("test", "XXX") }); formattedMessage.Should().Be("test {test}"); } [Fact] public void Should_ReturnMessage_When_NoArgs() { var formattedMessage = ArgHelper.FormatMessage("test {test}", new[] { new ArgPlaceholder() { Name = "test", Placeholder = "{test}" } }, Array.Empty()); formattedMessage.Should().Be("test {test}"); } [Fact] public void Should_ReturnMessage_When_NullArgs() { var formattedMessage = ArgHelper.FormatMessage("test {test}", new[] { new ArgPlaceholder() { Name = "test", Placeholder = "{test}" } }, null); formattedMessage.Should().Be("test {test}"); } [Fact] public void Should_FormatMessage_SinglePlaceholder() { var parametersChecked = 0; var testArg = new TestArg(parameters => { parameters.Should().BeNull(); parametersChecked++; }); testArg.AllowedParameters = Array.Empty(); testArg.Name = "test"; testArg.Value = "testValue"; var formattedMessage = ArgHelper.FormatMessage( "test {test}", new[] { new ArgPlaceholder() { Name = "test", Placeholder = "{test}", Parameters = null }, }, new IArg[] { testArg }); formattedMessage.Should().Be("test testValue"); parametersChecked.Should().Be(1); } [Fact] public void Should_FormatMessage_SinglePlaceholder_ManyOccurrencesInMessage() { var parametersChecked = 0; var testArg = new TestArg(parameters => { parameters.Should().BeNull(); parametersChecked++; }); testArg.AllowedParameters = Array.Empty(); testArg.Name = "test"; testArg.Value = "testValue"; var formattedMessage = ArgHelper.FormatMessage( "test {test} {test} {test}", new[] { new ArgPlaceholder() { Name = "test", Placeholder = "{test}", Parameters = null }, }, new IArg[] { testArg }); formattedMessage.Should().Be("test testValue testValue testValue"); parametersChecked.Should().Be(1); } [Fact] public void Should_FormatMessage_ManyPlaceholders_SingleArg() { var parametersSet = new[] { new Dictionary() { ["p1"] = "v1", }, new Dictionary() { ["p2"] = "v2", }, new Dictionary() { ["p3"] = "v3", } }; var parametersChecked1 = 0; var testArg1 = new TestArg(parameters => { parameters.Should().BeSameAs(parametersSet[parametersChecked1]); parametersChecked1++; }) { AllowedParameters = new[] { "p1", "p2", "p3" }, Name = "test1", Value = "testValue1", }; var formattedMessage = ArgHelper.FormatMessage( "test {test1|p1=v1} {test1|p2=v2} {test1|p3=v3}", new[] { new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|p1=v1}", Parameters = parametersSet[0], }, new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|p2=v2}", Parameters = parametersSet[1], }, new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|p3=v3}", Parameters = parametersSet[2], }, }, new IArg[] { testArg1 }); formattedMessage.Should().Be("test testValue1 testValue1 testValue1"); parametersChecked1.Should().Be(3); } [Fact] public void Should_FormatMessage_ManyArgs_ManyPlaceholders() { var parametersChecked1 = 0; var testArg1 = new TestArg(parameters => { parameters.Should().BeNull(); parametersChecked1++; }) { AllowedParameters = Array.Empty(), Name = "test1", Value = "testValue1", }; var parametersChecked2 = 0; var testArg2 = new TestArg(parameters => { parameters.Should().BeNull(); parametersChecked2++; }) { AllowedParameters = Array.Empty(), Name = "test2", Value = "testValue2", }; var formattedMessage = ArgHelper.FormatMessage( "test {test1} {test2} {test1}", new[] { new ArgPlaceholder() { Name = "test1", Placeholder = "{test1}", Parameters = null }, new ArgPlaceholder() { Name = "test2", Placeholder = "{test2}", Parameters = null }, }, new IArg[] { testArg1, testArg2 }); formattedMessage.Should().Be("test testValue1 testValue2 testValue1"); parametersChecked1.Should().Be(1); parametersChecked2.Should().Be(1); } [Fact] public void Should_FormatMessage_ManyArgs_ManyPlaceholders_ManyParams() { var paramSet1 = new Dictionary() { ["p1"] = "v1", }; var parametersChecked1 = 0; var testArg1 = new TestArg(parameters => { parameters.Should().BeSameAs(paramSet1); parametersChecked1++; }) { AllowedParameters = new[] { "p1" }, Name = "test1", Value = "testValue1", }; var paramSet2 = new Dictionary() { ["p2"] = "v2", }; var parametersChecked2 = 0; var testArg2 = new TestArg(parameters => { parameters.Should().BeSameAs(paramSet2); parametersChecked2++; }) { AllowedParameters = new[] { "p2" }, Name = "test2", Value = "testValue2", }; var formattedMessage = ArgHelper.FormatMessage( "test {test1|p1=v1} {test2|p2=v2} {test1|p1=v1}", new[] { new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|p1=v1}", Parameters = paramSet1, }, new ArgPlaceholder() { Name = "test2", Placeholder = "{test2|p2=v2}", Parameters = paramSet2 }, }, new IArg[] { testArg1, testArg2 }); formattedMessage.Should().Be("test testValue1 testValue2 testValue1"); parametersChecked1.Should().Be(1); parametersChecked2.Should().Be(1); } [Fact] public void Should_FormatMessage_PassParametersToArgs() { var parameters = new Dictionary() { ["param1"] = "paramValue1", }; var parametersChecked1 = 0; var testArg1 = new TestArg(p => { p.Should().BeSameAs(parameters); parametersChecked1++; }) { AllowedParameters = new[] { "param1" }, Name = "test1", Value = "testValue1", }; var formattedMessage = ArgHelper.FormatMessage( "test1 {test1|param1=paramValue1}", new[] { new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|param1=paramValue1}", Parameters = parameters }, }, new IArg[] { testArg1 }); formattedMessage.Should().Be("test1 testValue1"); parametersChecked1.Should().Be(1); } [Fact] public void Should_FormatMessage_NotPassParametersToArgs_When_AtLeastOneParameterNotAllowed() { var parameters = new Dictionary() { ["param1"] = "paramValue1", ["param2"] = "paramValue2", }; var parametersChecked1 = 0; var testArg1 = new TestArg(p => { p.Should().BeSameAs(parameters); parametersChecked1++; }) { AllowedParameters = new[] { "param1", "param2" }, Name = "test1", Value = "testValue1", }; var formattedMessage = ArgHelper.FormatMessage( "test1 {test1|param1=paramValue1|param2=paramValue2} {test1|param1=paramValue1|param2=paramValue2|invalidParameter=someValue}", new[] { new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|param1=paramValue1|param2=paramValue2}", Parameters = parameters, }, new ArgPlaceholder() { Name = "test1", Placeholder = "{test1|param1=paramValue1|param2=paramValue2|invalidParameter=someValue}", Parameters = new Dictionary() { ["param1"] = "paramValue1", ["param2"] = "paramValue2", ["invalidParameter"] = "someValue" }, }, }, new IArg[] { testArg1 }); formattedMessage.Should().Be("test1 testValue1 {test1|param1=paramValue1|param2=paramValue2|invalidParameter=someValue}"); parametersChecked1.Should().Be(1); } private class TestArg : IArg { private readonly Action> _parametersCheck; public TestArg(Action> parametersCheck) { _parametersCheck = parametersCheck; } public string Value { get; set; } public string Name { get; set; } public IReadOnlyCollection AllowedParameters { get; set; } public string ToString(IReadOnlyDictionary parameters) { _parametersCheck?.Invoke(parameters); return Value; } } } public class ExtractPlaceholders { [Theory] [InlineData("{single stuff")] [InlineData("single stuff}")] [InlineData("single }{ stuff")] [InlineData("single stuff")] [InlineData("")] public void Should_ReturnEmptyCollection_When_NoVariable(string message) { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders(message); placeholders.Should().BeEmpty(); } [Theory] [InlineData("abc {invalid|param1=value1param1=value2} def")] [InlineData("abc {invalid|param1value1param1value2} def")] [InlineData("abc {invalid|=} def")] [InlineData("abc {invalid||} def")] public void Should_ReturnEmptyCollection_When_InvalidParameters(string message) { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders(message); placeholders.Should().BeEmpty(); } [Theory] [InlineData("abc {|param1=value1} def")] [InlineData("abc { |param1=value1} def")] public void Should_ReturnEmptyCollection_When_EmptyName(string message) { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders(message); placeholders.Should().BeEmpty(); } [Fact] public void Should_Extract_And_SquashDuplicates() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {single|param1=value1} def {single|param1=value1}"); placeholders.Should().ContainSingle(); placeholders.Single().Placeholder.Should().Be("{single|param1=value1}"); placeholders.Single().Name.Should().Be("single"); placeholders.Single().Parameters.Should().ContainSingle(); placeholders.Single().Parameters.Single().Key.Should().Be("param1"); placeholders.Single().Parameters.Single().Value.Should().Be("value1"); } [Fact] public void Should_Parse_When_ManySameVariables_With_DifferentParameters() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {first|p1=v1} {first|p21=v21|p22=v22} def {first}"); placeholders.Count.Should().Be(3); placeholders.ElementAt(0).Placeholder.Should().Be("{first|p1=v1}"); placeholders.ElementAt(0).Name.Should().Be("first"); placeholders.ElementAt(0).Parameters.Should().ContainSingle(); placeholders.ElementAt(0).Parameters.Single().Key.Should().Be("p1"); placeholders.ElementAt(0).Parameters.Single().Value.Should().Be("v1"); placeholders.ElementAt(1).Placeholder.Should().Be("{first|p21=v21|p22=v22}"); placeholders.ElementAt(1).Name.Should().Be("first"); placeholders.ElementAt(1).Parameters.Count.Should().Be(2); placeholders.ElementAt(1).Parameters.ElementAt(0).Key.Should().Be("p21"); placeholders.ElementAt(1).Parameters.ElementAt(0).Value.Should().Be("v21"); placeholders.ElementAt(1).Parameters.ElementAt(1).Key.Should().Be("p22"); placeholders.ElementAt(1).Parameters.ElementAt(1).Value.Should().Be("v22"); placeholders.ElementAt(2).Placeholder.Should().Be("{first}"); placeholders.ElementAt(2).Name.Should().Be("first"); placeholders.ElementAt(2).Parameters.Should().BeEmpty(); } [Fact] public void Should_Parse_When_ManyVariables() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {first} {second} def {third}"); placeholders.Count.Should().Be(3); placeholders.ElementAt(0).Placeholder.Should().Be("{first}"); placeholders.ElementAt(0).Name.Should().Be("first"); placeholders.ElementAt(0).Parameters.Should().BeEmpty(); placeholders.ElementAt(1).Placeholder.Should().Be("{second}"); placeholders.ElementAt(1).Name.Should().Be("second"); placeholders.ElementAt(1).Parameters.Should().BeEmpty(); placeholders.ElementAt(2).Placeholder.Should().Be("{third}"); placeholders.ElementAt(2).Name.Should().Be("third"); placeholders.ElementAt(2).Parameters.Should().BeEmpty(); } [Fact] public void Should_Parse_When_ManyVariables_With_Parameters() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {first|p1=v1} {second|p21=v21|p22=v22} def {third|p31=v31|p32=v32|p33=v33}"); placeholders.Count.Should().Be(3); placeholders.ElementAt(0).Placeholder.Should().Be("{first|p1=v1}"); placeholders.ElementAt(0).Name.Should().Be("first"); placeholders.ElementAt(0).Parameters.Count.Should().Be(1); placeholders.ElementAt(0).Parameters.ElementAt(0).Key.Should().Be("p1"); placeholders.ElementAt(0).Parameters.ElementAt(0).Value.Should().Be("v1"); placeholders.ElementAt(1).Placeholder.Should().Be("{second|p21=v21|p22=v22}"); placeholders.ElementAt(1).Name.Should().Be("second"); placeholders.ElementAt(1).Parameters.Count.Should().Be(2); placeholders.ElementAt(1).Parameters.ElementAt(0).Key.Should().Be("p21"); placeholders.ElementAt(1).Parameters.ElementAt(0).Value.Should().Be("v21"); placeholders.ElementAt(1).Parameters.ElementAt(1).Key.Should().Be("p22"); placeholders.ElementAt(1).Parameters.ElementAt(1).Value.Should().Be("v22"); placeholders.ElementAt(2).Placeholder.Should().Be("{third|p31=v31|p32=v32|p33=v33}"); placeholders.ElementAt(2).Name.Should().Be("third"); placeholders.ElementAt(2).Parameters.Count.Should().Be(3); placeholders.ElementAt(2).Parameters.ElementAt(0).Key.Should().Be("p31"); placeholders.ElementAt(2).Parameters.ElementAt(0).Value.Should().Be("v31"); placeholders.ElementAt(2).Parameters.ElementAt(1).Key.Should().Be("p32"); placeholders.ElementAt(2).Parameters.ElementAt(1).Value.Should().Be("v32"); placeholders.ElementAt(2).Parameters.ElementAt(2).Key.Should().Be("p33"); placeholders.ElementAt(2).Parameters.ElementAt(2).Value.Should().Be("v33"); } [Fact] public void Should_Parse_When_SingleVariable() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {single} def"); placeholders.Count.Should().Be(1); placeholders.ElementAt(0).Placeholder.Should().Be("{single}"); placeholders.ElementAt(0).Name.Should().Be("single"); placeholders.ElementAt(0).Parameters.Count.Should().Be(0); } [Fact] public void Should_Parse_When_SingleVariable_With_ManyParameters() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {single|param1=value1|param2=value2|param3=value3} def"); placeholders.Count.Should().Be(1); placeholders.ElementAt(0).Placeholder.Should().Be("{single|param1=value1|param2=value2|param3=value3}"); placeholders.ElementAt(0).Name.Should().Be("single"); placeholders.ElementAt(0).Parameters.Count.Should().Be(3); placeholders.ElementAt(0).Parameters.ElementAt(0).Key.Should().Be("param1"); placeholders.ElementAt(0).Parameters.ElementAt(0).Value.Should().Be("value1"); placeholders.ElementAt(0).Parameters.ElementAt(1).Key.Should().Be("param2"); placeholders.ElementAt(0).Parameters.ElementAt(1).Value.Should().Be("value2"); placeholders.ElementAt(0).Parameters.ElementAt(2).Key.Should().Be("param3"); placeholders.ElementAt(0).Parameters.ElementAt(2).Value.Should().Be("value3"); } [Fact] public void Should_Parse_When_SingleVariable_With_SingleParameter() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {single|param1=value1} def"); placeholders.Count.Should().Be(1); placeholders.ElementAt(0).Placeholder.Should().Be("{single|param1=value1}"); placeholders.ElementAt(0).Name.Should().Be("single"); placeholders.ElementAt(0).Parameters.Count.Should().Be(1); placeholders.ElementAt(0).Parameters.ElementAt(0).Key.Should().Be("param1"); placeholders.ElementAt(0).Parameters.ElementAt(0).Value.Should().Be("value1"); } [Fact] public void Should_ParseOnlyValidOnes() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("{valid} abc {invalid|param1=value1|param1=value2} {valid2|param=value} def {invalid|param1=value1param1=value2} xyz {invalid2|param1value1param1value2}"); placeholders.Count.Should().Be(2); placeholders.ElementAt(0).Placeholder.Should().Be("{valid}"); placeholders.ElementAt(0).Name.Should().Be("valid"); placeholders.ElementAt(0).Parameters.Count.Should().Be(0); placeholders.ElementAt(1).Placeholder.Should().Be("{valid2|param=value}"); placeholders.ElementAt(1).Name.Should().Be("valid2"); placeholders.ElementAt(1).Parameters.Count.Should().Be(1); placeholders.ElementAt(1).Parameters.ElementAt(0).Key.Should().Be("param"); placeholders.ElementAt(1).Parameters.ElementAt(0).Value.Should().Be("value"); } [Fact] public void Should_ReturnEmptyCollection_When_DuplicateParameter() { IReadOnlyCollection placeholders = ArgHelper.ExtractPlaceholders("abc {invalid|param1=value1|param1=value2} def"); placeholders.Should().BeEmpty(); } [Fact] public void Should_ThrowException_When_NullMessage() { Action action = () => ArgHelper.ExtractPlaceholders(null); action.Should().ThrowExactly(); } } [Fact] public void Assignment_Should_BeEqualitySign() { ArgHelper.Assignment.Should().Be('='); } [Fact] public void Divider_Should_BeVerticalBar() { ArgHelper.Divider.Should().Be('|'); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/EnumArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using System.IO; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class EnumArgTests { [Theory] [InlineData(StringComparison.Ordinal, "G", "Ordinal")] [InlineData(StringComparison.Ordinal, "D", "4")] [InlineData(StringComparison.Ordinal, "X", "00000004")] public void Should_Stringify_UsingFormat(StringComparison stringComparison, string format, string expectedString) { IArg arg = Arg.Enum("name", stringComparison); var stringified = arg.ToString(new Dictionary { ["format"] = format }); stringified.Should().Be(expectedString); } [Fact] public void Should_Initialize() { IArg arg = Arg.Enum("name", StringComparison.CurrentCulture); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(2); arg.AllowedParameters.Should().Contain("translation"); arg.AllowedParameters.Should().Contain("format"); arg.Should().BeOfType>(); ((EnumArg)arg).Value.Should().Be(StringComparison.CurrentCulture); } [Fact] public void Should_NotStringify_Translation_When_ParameterValueIsNotTrue() { IArg arg1 = Arg.Enum("name", StringComparison.CurrentCulture); var stringified1 = arg1.ToString(new Dictionary { ["translation"] = "false" }); IArg arg2 = Arg.Enum("name", FileMode.OpenOrCreate); var stringified2 = arg2.ToString(new Dictionary { ["translation"] = "somevalue" }); stringified1.Should().Be("CurrentCulture"); stringified2.Should().Be("OpenOrCreate"); } [Fact] public void Should_Stringify_Translation() { IArg arg1 = Arg.Enum("name", StringComparison.CurrentCulture); var stringified1 = arg1.ToString(new Dictionary { ["translation"] = "true" }); IArg arg2 = Arg.Enum("name", FileMode.OpenOrCreate); var stringified2 = arg2.ToString(new Dictionary { ["translation"] = "true" }); stringified1.Should().Be("{_translation|key=Enum.System.StringComparison.CurrentCulture}"); stringified2.Should().Be("{_translation|key=Enum.System.IO.FileMode.OpenOrCreate}"); } [Fact] public void Should_StringifyDefaultValues() { IArg arg = Arg.Enum("name", StringComparison.CurrentCulture); arg.Name.Should().Be("name"); arg.ToString(null).Should().Be("CurrentCulture"); } [Fact] public void Should_StringifyUsingTranslation_When_BothFormatAndTranslationPresent() { IArg arg1 = Arg.Enum("name", StringComparison.CurrentCulture); var stringified1 = arg1.ToString(new Dictionary { ["format"] = "D", ["translation"] = "true", }); stringified1.Should().Be("{_translation|key=Enum.System.StringComparison.CurrentCulture}"); } [Fact] public void Should_ThrowException_When_NullName() { Action action = () => { Arg.Enum(null, StringComparison.CurrentCulture); }; action.Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/GuidArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class GuidArgTests { [Theory] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", null, "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "upper", "C2CE1F3B-17E5-412E-923B-6B4E268F31AA")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "lower", "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "something", "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] public void Should_Stringify_Case(string value, string caseParameter, string expectedString) { IArg arg = Arg.GuidValue("name", new Guid(value)); var stringified = arg.ToString(caseParameter != null ? new Dictionary { { "case", caseParameter } } : null); stringified.Should().Be(expectedString); } [Theory] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", null, "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "D", "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "B", "{c2ce1f3b-17e5-412e-923b-6b4e268f31aa}")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "P", "(c2ce1f3b-17e5-412e-923b-6b4e268f31aa)")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "X", "{0xc2ce1f3b,0x17e5,0x412e,{0x92,0x3b,0x6b,0x4e,0x26,0x8f,0x31,0xaa}}")] public void Should_Stringify_Format(string value, string format, string expectedString) { IArg arg = Arg.GuidValue("name", new Guid(value)); var stringified = arg.ToString(new Dictionary { { "format", format } }); stringified.Should().Be(expectedString); } [Theory] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", null, null, "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", null, "upper", "C2CE1F3B-17E5-412E-923B-6B4E268F31AA")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", null, "lower", "c2ce1f3b-17e5-412e-923b-6b4e268f31aa")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "B", null, "{c2ce1f3b-17e5-412e-923b-6b4e268f31aa}")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "B", "upper", "{C2CE1F3B-17E5-412E-923B-6B4E268F31AA}")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "B", "lower", "{c2ce1f3b-17e5-412e-923b-6b4e268f31aa}")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "X", null, "{0xc2ce1f3b,0x17e5,0x412e,{0x92,0x3b,0x6b,0x4e,0x26,0x8f,0x31,0xaa}}")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "X", "upper", "{0XC2CE1F3B,0X17E5,0X412E,{0X92,0X3B,0X6B,0X4E,0X26,0X8F,0X31,0XAA}}")] [InlineData("c2ce1f3b-17e5-412e-923b-6b4e268f31aa", "X", "lower", "{0xc2ce1f3b,0x17e5,0x412e,{0x92,0x3b,0x6b,0x4e,0x26,0x8f,0x31,0xaa}}")] public void Should_Stringify(string value, string format, string casing, string expectedString) { IArg arg = Arg.GuidValue("name", new Guid(value)); var stringified = arg.ToString(new Dictionary { { "format", format }, { "case", casing } }); stringified.Should().Be(expectedString); } [Fact] public void Should_Initialize() { IArg arg = Arg.GuidValue("name", new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(2); arg.AllowedParameters.Should().Contain("case"); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(2); arg.AllowedParameters.Should().Contain("case"); arg.AllowedParameters.Should().Contain("format"); arg.Should().BeOfType(); ((GuidArg)arg).Value.Should().Be(new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")); } [Fact] public void Should_Stringify_IntoSameValue_When_InvalidParameter() { IArg arg = Arg.GuidValue("name", new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")); var stringified = arg.ToString(new Dictionary { { "invalidParameter", "test" } }); stringified.Should().Be("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"); } [Fact] public void Should_ThrowException_When_NullName() { Action action = () => Arg.GuidValue(null, new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa")); action.Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/NameArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class NameArgTests { [Fact] public void Should_Initialize() { var arg = new NameArg("someName"); NameArg.Name.Should().Be("_name"); (arg as IArg).Name.Should().Be("_name"); arg.AllowedParameters.Count.Should().Be(1); arg.AllowedParameters.Should().Contain("format"); } [Fact] public void Should_Stringify_ReturnArgName_If_NullParameters() { var arg = new NameArg("someName"); var key = new Dictionary() { ["invalid_key"] = "value1" }; var stringified = arg.ToString(key); stringified.Should().Be("someName"); } [Fact] public void Should_Stringify_ReturnArgName_If_InvalidParameterName() { var arg = new NameArg("someName"); var key = new Dictionary() { ["invalid_key"] = "value1" }; var stringified = arg.ToString(key); stringified.Should().Be("someName"); } [Theory] [InlineData("ToGetYourGEDInTimeASongAboutThe26ABCsIsOfTheEssenceButAPersonalIDCardForUser456InRoom26AContainingABC26TimesIsNotAsEasyAs123ForC3POOrR2D2Or2R2D", "To Get Your GED In Time A Song About The 26 ABCs Is Of The Essence But A Personal ID Card For User 456 In Room 26A Containing ABC 26 Times Is Not As Easy As 123 For C3PO Or R2D2 Or 2R2D")] [InlineData("helloThere", "Hello There")] [InlineData("HelloThere", "Hello There")] [InlineData("ILoveTheUSA", "I Love The USA")] [InlineData("iLoveTheUSA", "I Love The USA")] [InlineData("DBHostCountry", "DB Host Country")] [InlineData("SetSlot123ToInput456", "Set Slot 123 To Input 456")] [InlineData("ILoveTheUSANetworkInTheUSA", "I Love The USA Network In The USA")] [InlineData("Limit_IOC_Duration", "Limit IOC Duration")] [InlineData("This_is_a_Test_of_Network123_in_12_days", "This Is A Test Of Network 123 In 12 Days")] [InlineData("ASongAboutTheABCsIsFunToSing", "A Song About The ABCs Is Fun To Sing")] [InlineData("CFDs", "CFDs")] [InlineData("DBSettings", "DB Settings")] [InlineData("IWouldLove1Apple", "I Would Love 1 Apple")] [InlineData("Employee22IsCool", "Employee 22 Is Cool")] [InlineData("SubIDIn", "Sub ID In")] [InlineData("ConfigureCFDsImmediately", "Configure CFDs Immediately")] [InlineData("UseTakerLoginForOnBehalfOfSubIDInOrders", "Use Taker Login For On Behalf Of Sub ID In Orders")] [InlineData("X", "X")] [InlineData("x", "X")] public void Should_Stringify_To_TitleCase(string name, string expectedTitleCase) { var arg = new NameArg(name); var stringified = arg.ToString(new Dictionary() { ["format"] = "titleCase" }); stringified.Should().Be(expectedTitleCase); } [Fact] public void Should_ThrowException_When_NullName() { new Action(() => { new NameArg(null); }).Should().ThrowExactly().And.ParamName.Should().Be("name"); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/NumberArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class NumberArgTests { public static IEnumerable Should_Stringify_Numbers_WithFormatAndCulture_Data() { yield return new object[] { Arg.Number("name", 123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (uint)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (short)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (ushort)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (long)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (ulong)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (byte)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (sbyte)123), "0.00", "pl-PL", "123,00", }; yield return new object[] { Arg.Number("name", (decimal)123.987), "0.00", "pl-PL", "123,99", }; yield return new object[] { Arg.Number("name", 123.987), "0.00", "pl-PL", "123,99", }; yield return new object[] { Arg.Number("name", (float)123.987), "0.00", "pl-PL", "123,99", }; } [Theory] [MemberData(nameof(Should_Stringify_Numbers_WithFormatAndCulture_Data))] public void Should_Stringify_Numbers_WithFormatAndCulture(NumberArg arg, string format, string culture, string expectedString) { var stringified = arg.ToString(new Dictionary { ["format"] = format, ["culture"] = culture, }); stringified.Should().Be(expectedString); } public static IEnumerable Should_Stringify_Dates_WithCulture_Data() { yield return new object[] { Arg.Number("name", 123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (uint)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (short)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (ushort)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (long)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (ulong)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (byte)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (sbyte)123), "pl-PL", "123", }; yield return new object[] { Arg.Number("name", (decimal)123.987), "pl-PL", "123,987", }; yield return new object[] { Arg.Number("name", 123.987), "pl-PL", "123,987", }; yield return new object[] { Arg.Number("name", (float)123.987), "pl-PL", "123,987", }; } [Theory] [MemberData(nameof(Should_Stringify_Dates_WithCulture_Data))] public void Should_Stringify_Dates_WithCulture(NumberArg arg, string culture, string expectedString) { var stringified = arg.ToString(new Dictionary { ["culture"] = culture, }); stringified.Should().Be(expectedString); } public static IEnumerable Should_Stringify_WithFormat_Data() { yield return new object[] { Arg.Number("name", 123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (uint)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (short)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (ushort)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (long)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (ulong)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (byte)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (sbyte)123), "0.00", "123.00", }; yield return new object[] { Arg.Number("name", (decimal)123.987), "0.00", "123.99", }; yield return new object[] { Arg.Number("name", 123.987), "0.00", "123.99", }; yield return new object[] { Arg.Number("name", (float)123.987), "0.00", "123.99", }; } [Theory] [MemberData(nameof(Should_Stringify_WithFormat_Data))] public void Should_Stringify_WithFormat(NumberArg arg, string format, string expectedString) { var stringified = arg.ToString(new Dictionary { ["format"] = format, }); stringified.Should().Be(expectedString); } public static IEnumerable Should_Stringify_Default_Data() { yield return new object[] { Arg.Number("name", 123), "123", }; yield return new object[] { Arg.Number("name", (uint)123), "123", }; yield return new object[] { Arg.Number("name", (short)123), "123", }; yield return new object[] { Arg.Number("name", (ushort)123), "123", }; yield return new object[] { Arg.Number("name", (long)123), "123", }; yield return new object[] { Arg.Number("name", (ulong)123), "123", }; yield return new object[] { Arg.Number("name", (byte)123), "123", }; yield return new object[] { Arg.Number("name", (sbyte)123), "123", }; yield return new object[] { Arg.Number("name", (decimal)123.987), "123.987", }; yield return new object[] { Arg.Number("name", 123.987), "123.987", }; yield return new object[] { Arg.Number("name", (float)123.987), "123.987", }; } [Theory] [MemberData(nameof(Should_Stringify_Default_Data))] public void Should_Stringify_Default(NumberArg arg, string expectedString) { var stringified = arg.ToString(null); stringified.Should().Be(expectedString); } [Fact] public void Should_Initialize() { IArg arg = Arg.Number("name", 1); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(2); arg.AllowedParameters.Should().Contain("format"); arg.AllowedParameters.Should().Contain("culture"); arg.Should().BeOfType>(); ((NumberArg)arg).Value.Should().Be(1); } [Fact] public void Should_Initialize_Values() { var argInt = Arg.Number("name", 1); argInt.Should().BeOfType>(); ((NumberArg)argInt).Value.Should().Be(1); var argUInt = Arg.Number("name", (uint)1); argUInt.Should().BeOfType>(); ((NumberArg)argUInt).Value.Should().Be(1); var argShort = Arg.Number("name", (short)1); argShort.Should().BeOfType>(); ((NumberArg)argShort).Value.Should().Be(1); var argLong = Arg.Number("name", (long)1); argLong.Should().BeOfType>(); ((NumberArg)argLong).Value.Should().Be(1); var argUShort = Arg.Number("name", (ushort)1); argUShort.Should().BeOfType>(); ((NumberArg)argUShort).Value.Should().Be(1); var argULong = Arg.Number("name", (ulong)1); argULong.Should().BeOfType>(); ((NumberArg)argULong).Value.Should().Be(1); var argDouble = Arg.Number("name", (double)1); argDouble.Should().BeOfType>(); ((NumberArg)argDouble).Value.Should().Be(1); var argFloat = Arg.Number("name", (float)1); argFloat.Should().BeOfType>(); ((NumberArg)argFloat).Value.Should().Be(1); var argByte = Arg.Number("name", (byte)1); argByte.Should().BeOfType>(); ((NumberArg)argByte).Value.Should().Be(1); var argSByte = Arg.Number("name", (sbyte)1); argSByte.Should().BeOfType>(); ((NumberArg)argSByte).Value.Should().Be(1); var argDecimal = Arg.Number("name", (decimal)1); argDecimal.Should().BeOfType>(); ((NumberArg)argDecimal).Value.Should().Be(1); } [Fact] public void Should_ThrowException_When_NullName() { new Action(() => { Arg.Number(null, 0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, 0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (uint)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (short)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (ushort)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (long)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (ulong)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (double)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (float)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (byte)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (sbyte)0); }).Should().ThrowExactly(); new Action(() => { Arg.Number(null, (decimal)0); }).Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/TextArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class TextArgTests { [Theory] [InlineData("TeSt", null, "TeSt")] [InlineData("TeSt", "upper", "TEST")] [InlineData("TeSt", "lower", "test")] [InlineData("TeSt", "something", "TeSt")] public void Should_Stringify_String(string value, string caseParameter, string expectedString) { var arg = Arg.Text("name", value); var stringified = arg.ToString(caseParameter != null ? new Dictionary { { "case", caseParameter } } : null); stringified.Should().Be(expectedString); } [Theory] [InlineData('t', null, "t")] [InlineData('t', "upper", "T")] [InlineData('T', "upper", "T")] [InlineData('t', "lower", "t")] [InlineData('T', "lower", "t")] public void Should_Stringify_Char(char value, string caseParameter, string expectedString) { var arg = Arg.Text("name", value); var stringified = arg.ToString(caseParameter != null ? new Dictionary { { "case", caseParameter }, } : null); stringified.Should().Be(expectedString); } [Theory] [InlineData('t', "t")] [InlineData('T', "T")] public void Should_Stringify_Char_IntoSameValue_When_InvalidParameter(char value, string expectedString) { var arg = Arg.Text("name", value); var stringified = arg.ToString(new Dictionary { { "invalidParameter", "test" }, }); stringified.Should().Be(expectedString); } [Fact] public void Should_Initialize() { var arg = Arg.Text("name", "value"); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(1); arg.AllowedParameters.Should().Contain("case"); arg.Should().BeOfType(); ((TextArg)arg).Value.Should().Be("value"); } [Fact] public void Should_Stringify_String_IntoSameValue_When_InvalidParameter() { var arg = Arg.Text("name", "nAmE"); var stringified = arg.ToString(new Dictionary { { "invalidParameter", "test" }, }); stringified.Should().Be("nAmE"); } [Fact] public void Should_ThrowException_When_NullName() { Action action = () => { Arg.Text(null, "value"); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullValue() { Action action = () => { Arg.Text("name", null); }; action.Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/TimeArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using System.Globalization; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class TimeArgTests { public static IEnumerable Should_Stringify_Times_WithFormatAndCulture_Data() { yield return new object[] { Arg.Time("name", new DateTime(2000, 01, 15, 16, 04, 05, 06)), "s", "en-US", "2000-01-15T16:04:05" }; yield return new object[] { Arg.Time("name", new DateTimeOffset(2000, 01, 15, 16, 04, 05, 06, TimeSpan.Zero)), "s", "en-US", "2000-01-15T16:04:05" }; yield return new object[] { Arg.Time("name", new TimeSpan(1, 2, 3)), "g", "en-US", "1:02:03" }; } [Theory] [MemberData(nameof(Should_Stringify_Times_WithFormatAndCulture_Data))] public void Should_Stringify_Times_WithFormatAndCulture(TimeArg arg, string format, string culture, string expectedString) { var stringified = arg.ToString(new Dictionary { { "format", format }, { "culture", culture } }); stringified.Should().Be(expectedString); } public static IEnumerable Should_Stringify_Dates_WithCulture_Data() { yield return new object[] { Arg.Time("name", new DateTime(2000, 01, 15, 16, 04, 05, 06)), "en-US", new DateTime(2000, 01, 15, 16, 04, 05, 06).ToString(CultureInfo.GetCultureInfo("en-US")) }; yield return new object[] { Arg.Time("name", new DateTimeOffset(2000, 01, 15, 16, 04, 05, 06, TimeSpan.Zero)), "en-US", new DateTimeOffset(2000, 01, 15, 16, 04, 05, 06, TimeSpan.Zero).ToString(CultureInfo.GetCultureInfo("en-US")) }; yield return new object[] { Arg.Time("name", new TimeSpan(1, 2, 3)), "en-US", "01:02:03" }; } [Theory] [MemberData(nameof(Should_Stringify_Dates_WithCulture_Data))] public void Should_Stringify_Dates_WithCulture(TimeArg arg, string culture, string expectedString) { var stringified = arg.ToString(new Dictionary { { "culture", culture } }); stringified.Should().Be(expectedString); } public static IEnumerable Should_Stringify_WithFormat_Data() { yield return new object[] { Arg.Time("name", new DateTime(2000, 01, 15, 16, 04, 05, 06)), "s", "2000-01-15T16:04:05" }; yield return new object[] { Arg.Time("name", new DateTime(2000, 01, 15, 16, 04, 05, 06)), "yyyyMMdd", "20000115" }; yield return new object[] { Arg.Time("name", new DateTimeOffset(2000, 01, 15, 16, 04, 05, 06, TimeSpan.Zero)), "s", "2000-01-15T16:04:05" }; yield return new object[] { Arg.Time("name", new DateTimeOffset(2000, 01, 15, 16, 04, 05, 06, TimeSpan.Zero)), "yyyyMMdd", "20000115" }; yield return new object[] { Arg.Time("name", new TimeSpan(1, 2, 3)), "g", "1:02:03" }; } [Theory] [MemberData(nameof(Should_Stringify_WithFormat_Data))] public void Should_Stringify_WithFormat(TimeArg arg, string format, string expectedString) { var stringified = arg.ToString(new Dictionary { { "format", format } }); stringified.Should().Be(expectedString); } public static IEnumerable Should_Stringify_Default_Data() { yield return new object[] { Arg.Time("name", new DateTime(2000, 01, 15, 16, 04, 05, 06)), "2000-01-15 16:04:05.006" }; yield return new object[] { Arg.Time("name", new DateTimeOffset(2000, 01, 15, 16, 04, 05, 06, TimeSpan.Zero)), "2000-01-15 16:04:05.006" }; yield return new object[] { Arg.Time("name", new TimeSpan(1, 2, 3)), "01:02:03" }; } [Theory] [MemberData(nameof(Should_Stringify_Default_Data))] public void Should_Stringify_Default(TimeArg arg, string expectedString) { var stringified = arg.ToString(null); stringified.Should().Be(expectedString); } [Fact] public void Should_Initialize() { IArg arg = Arg.Number("name", 1); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(2); arg.AllowedParameters.Should().Contain("format"); arg.AllowedParameters.Should().Contain("culture"); } [Fact] public void Should_ThrowException_When_NullName() { new Action(() => { Arg.Time(null, TimeSpan.FromTicks(0)); }).Should().ThrowExactly(); new Action(() => { Arg.Time(null, new DateTime(0)); }).Should().ThrowExactly(); new Action(() => { Arg.Time(null, new DateTimeOffset(0, TimeSpan.Zero)); }).Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/TranslationArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class TranslationArgTests { [Fact] public void Should_Initialize() { var translation = new Dictionary(); var arg = new TranslationArg(translation); TranslationArg.Name.Should().Be("_translation"); (arg as IArg).Name.Should().Be("_translation"); arg.AllowedParameters.Count.Should().Be(1); arg.AllowedParameters.Should().Contain("key"); } [Theory] [InlineData("test", "{_translation|key=test}")] [InlineData("Some.Sample.Value", "{_translation|key=Some.Sample.Value}")] public void Should_CreatePlaceHolder(string key, string expectedPlaceholder) { var placeholder = TranslationArg.CreatePlaceholder(key); placeholder.Should().Be(expectedPlaceholder); } [Fact] public void Should_Stringify_UsingDictionary_And_Key() { var translation = new Dictionary() { ["key1"] = "value1", ["key2"] = "value2", }; var arg = new TranslationArg(translation); var key1 = arg.ToString(new Dictionary() { ["key"] = "key1", }); var key2 = arg.ToString(new Dictionary() { ["key"] = "key2", }); key1.Should().Be("value1"); key2.Should().Be("value2"); } [Fact] public void Should_Stringify_ReturnKey_If_KeyNotInTranslation() { var translation = new Dictionary() { ["key1"] = "value1", ["key2"] = "value2", }; var arg = new TranslationArg(translation); var key1 = arg.ToString(new Dictionary() { ["key"] = "invalid", }); var key2 = arg.ToString(new Dictionary() { ["key"] = "KEY1", }); key1.Should().Be("invalid"); key2.Should().Be("KEY1"); } [Fact] public void Should_Stringify_ReturnArgName_If_NullParameters() { var translation = new Dictionary() { ["key1"] = "value1", ["key2"] = "value2", }; var arg = new TranslationArg(translation); var name = arg.ToString(null); name.Should().Be("_translation"); } [Fact] public void Should_Stringify_ReturnArgName_If_InvalidParameterName() { var translation = new Dictionary() { ["key1"] = "value1", ["key2"] = "value2", }; var arg = new TranslationArg(translation); var key = new Dictionary() { ["invalid_key"] = "value1" }; var name = arg.ToString(key); name.Should().Be("_translation"); } [Fact] public void Should_ThrowException_When_NullKey() { new Action(() => { new TranslationArg(null); }).Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Args/TypeArgTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Args { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors.Args; using Xunit; public class TypeArgTests { [Fact] public void Should_Initialize() { var arg = Arg.Type("name", typeof(int)); arg.Name.Should().Be("name"); arg.AllowedParameters.Count.Should().Be(2); arg.AllowedParameters.Should().Contain("translation"); arg.AllowedParameters.Should().Contain("format"); arg.Should().BeOfType(); ((TypeArg)arg).Value.Should().Be(typeof(int)); } [Theory] [InlineData(typeof(int), "name", "Int32")] [InlineData(typeof(int), "fullName", "System.Int32")] [InlineData(typeof(int), "toString", "System.Int32")] [InlineData(typeof(Nullable), "name", "Nullable")] [InlineData(typeof(Nullable), "fullName", "System.Nullable")] [InlineData(typeof(Nullable), "toString", "System.Nullable`1[System.Int32]")] public void Should_Stringify_Format(Type value, string format, string expectedString) { var arg = Arg.Type("name", value); var stringified = arg.ToString(new Dictionary { ["format"] = format, }); stringified.Should().Be(expectedString); } [Fact] public void Should_Stringify_Translation() { IArg arg1 = Arg.Type("name", typeof(StringComparison)); var stringified1 = arg1.ToString(new Dictionary { ["translation"] = "true", }); IArg arg2 = Arg.Type("name", typeof(Nullable)); var stringified2 = arg2.ToString(new Dictionary { ["translation"] = "true", }); stringified1.Should().Be("{_translation|key=Type.System.StringComparison}"); stringified2.Should().Be("{_translation|key=Type.System.Nullable}"); } [Fact] public void Should_NotStringify_Translation_When_ParameterValueIsNotTrue() { IArg arg1 = Arg.Type("name", typeof(StringComparison)); var stringified1 = arg1.ToString(new Dictionary { ["translation"] = "false", }); IArg arg2 = Arg.Type("name", typeof(Nullable)); var stringified2 = arg2.ToString(new Dictionary { ["translation"] = "TRUE", }); stringified1.Should().Be("StringComparison"); stringified2.Should().Be("Nullable"); } [Fact] public void Should_StringifyUsingTranslation_When_BothFormatAndTranslationPresent() { IArg arg1 = Arg.Type("name", typeof(Nullable)); var stringified1 = arg1.ToString(new Dictionary { ["format"] = "toString", ["translation"] = "true", }); stringified1.Should().Be("{_translation|key=Type.System.Nullable}"); } [Fact] public void Should_StringifyDefaultValues() { IArg arg = Arg.Type("name", typeof(Nullable)); arg.Name.Should().Be("name"); arg.ToString(null).Should().Be("Nullable"); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/MessageCacheTests.cs ================================================ namespace Validot.Tests.Unit.Errors { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Errors; using Validot.Errors.Args; using Xunit; public class MessageCacheTests { public class AddMessage { [Fact] public void Should_AddMessage() { var messageCache = new MessageCache(); messageCache.AddMessage( "trans1", 1, new[] { "message1", "message2" }); var cachedMessages = messageCache.GetMessages("trans1", 1); cachedMessages.Count.Should().Be(2); cachedMessages.Should().Contain("message1"); cachedMessages.Should().Contain("message2"); } [Fact] public void Should_AddMessage_MultipleTimes_ToDifferentDictionaries() { var messageCache = new MessageCache(); messageCache.AddMessage( "trans1", 1, new[] { "message1" }); messageCache.AddMessage( "trans2", 2, new[] { "message2" }); var cachedMessages1 = messageCache.GetMessages("trans1", 1); cachedMessages1.Count.Should().Be(1); cachedMessages1.Should().Contain("message1"); var cachedMessages2 = messageCache.GetMessages("trans2", 2); cachedMessages2.Count.Should().Be(1); cachedMessages2.Should().Contain("message2"); } [Fact] public void Should_AddMessage_MultipleTimes_ToDifferentDictionaries_SameErrorId() { var messageCache = new MessageCache(); messageCache.AddMessage( "trans1", 1, new[] { "message1" }); messageCache.AddMessage( "trans2", 1, new[] { "message2" }); var cachedMessages1 = messageCache.GetMessages("trans1", 1); cachedMessages1.Count.Should().Be(1); cachedMessages1.Should().Contain("message1"); var cachedMessages2 = messageCache.GetMessages("trans2", 1); cachedMessages2.Count.Should().Be(1); cachedMessages2.Should().Contain("message2"); } [Fact] public void Should_ThrowException_When_AddingMultipleTimes_ToSameDictionary_And_ToSameError() { var messageCache = new MessageCache(); messageCache.AddMessage( "trans1", 1, new[] { "message1" }); Action action = () => messageCache.AddMessage( "trans1", 1, new[] { "message2" }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_MessagesCollectionContainsNull() { var messageCache = new MessageCache(); Action action = () => messageCache.AddMessage( "trans1", 1, new[] { "message1", null }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullMessagesCollection() { var messageCache = new MessageCache(); Action action = () => messageCache.AddMessage( "trans1", 1, null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslationName() { var messageCache = new MessageCache(); Action action = () => messageCache.AddMessage( null, 1, new[] { "message2" }); action.Should().ThrowExactly(); } } public class GetMessages { [Fact] public void Should_GetMessages() { var messageCache = new MessageCache(); var messages = new[] { "message1", "message2", "message3" }; messageCache.AddMessage( "trans1", 1, messages); var cachedMessages1 = messageCache.GetMessages("trans1", 1); cachedMessages1.Should().BeSameAs(messages); cachedMessages1.Count.Should().Be(3); cachedMessages1.Should().Contain("message1"); cachedMessages1.Should().Contain("message2"); cachedMessages1.Should().Contain("message3"); } [Fact] public void Should_ThrowException_When_InvalidErrorId() { var messageCache = new MessageCache(); messageCache.AddMessage("translation", 1, new[] { "message" }); Action action = () => { messageCache.GetMessages("translation", 123); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidTranslationName() { var messageCache = new MessageCache(); messageCache.AddMessage("translation", 1, new[] { "message" }); Action action = () => { messageCache.GetMessages("invalid_translation", 1); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslationName() { var messageCache = new MessageCache(); Action action = () => { messageCache.GetMessages(null, 1); }; action.Should().ThrowExactly(); } } public class GetMessageAmount { public static IEnumerable Should_GetMessageAmount_Data() { var errorMessages = new Dictionary { [1] = new[] { "1", "2", "3" }, [2] = new[] { "1", "2", "3", "4", "5" }, [3] = new[] { "1", "2", "3", "4", "5", "6", "7" }, [4] = new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9" } }; yield return new object[] { errorMessages, new List { 1, 3, 4 }, 19 }; yield return new object[] { errorMessages, new List { 1 }, 3 }; yield return new object[] { errorMessages, new List { 2, 4 }, 14 }; yield return new object[] { errorMessages, new List { 1, 2 }, 8 }; } [Theory] [MemberData(nameof(Should_GetMessageAmount_Data))] public void Should_GetMessageAmount(Dictionary messages, List errorsIdsToCheck, int expectedErrorsAmount) { var messageCache = new MessageCache(); foreach (KeyValuePair pair in messages) { messageCache.AddMessage("translation", pair.Key, pair.Value); } var amount = messageCache.GetMessageAmount(errorsIdsToCheck); amount.Should().Be(expectedErrorsAmount); } [Fact] public void Should_NotModifyList() { var messageCache = new MessageCache(); var list = new List { 1, 2, 3, 4, 5 }; foreach (var id in list) { messageCache.AddMessage("translation", id, new[] { "message " }); } messageCache.GetMessageAmount(list); list.Count.Should().Be(5); list.ElementAt(0).Should().Be(1); list.ElementAt(1).Should().Be(2); list.ElementAt(2).Should().Be(3); list.ElementAt(3).Should().Be(4); list.ElementAt(4).Should().Be(5); } [Fact] public void Should_ThrowException_When_NullIdsList() { var messageCache = new MessageCache(); Action action = () => { messageCache.GetMessageAmount(null); }; action.Should().ThrowExactly(); } } public class AddMessageWithPathArgs { [Fact] public void Should_AddMessageWithPathArgs_MultipleErrors() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); messageCache.AddMessageWithPathArgs("translation", "path", 2, new[] { "message2" }); var messages1 = messageCache.GetMessagesWithPathArgs("translation", "path", 1); messages1.Count.Should().Be(1); messages1.ElementAt(0).Should().Be("message1"); var messages2 = messageCache.GetMessagesWithPathArgs("translation", "path", 2); messages2.Count.Should().Be(1); messages2.ElementAt(0).Should().Be("message2"); } [Fact] public void Should_AddMessageWithPathArgs_MultiplePaths() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path1", 1, new[] { "message1" }); messageCache.AddMessageWithPathArgs("translation", "path1", 2, new[] { "message2" }); messageCache.AddMessageWithPathArgs("translation", "path2", 1, new[] { "message3" }); var messages1 = messageCache.GetMessagesWithPathArgs("translation", "path1", 1); messages1.Count.Should().Be(1); messages1.ElementAt(0).Should().Be("message1"); var messages2 = messageCache.GetMessagesWithPathArgs("translation", "path1", 2); messages2.Count.Should().Be(1); messages2.ElementAt(0).Should().Be("message2"); var messages3 = messageCache.GetMessagesWithPathArgs("translation", "path2", 1); messages3.Count.Should().Be(1); messages3.ElementAt(0).Should().Be("message3"); } [Fact] public void Should_AddMessageWithPathArgs_MultipleTranslations() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation1", "path1", 1, new[] { "message1" }); messageCache.AddMessageWithPathArgs("translation1", "path1", 2, new[] { "message2" }); messageCache.AddMessageWithPathArgs("translation1", "path2", 1, new[] { "message3" }); messageCache.AddMessageWithPathArgs("translation2", "path1", 1, new[] { "message4" }); var messages1 = messageCache.GetMessagesWithPathArgs("translation1", "path1", 1); messages1.Count.Should().Be(1); messages1.ElementAt(0).Should().Be("message1"); var messages2 = messageCache.GetMessagesWithPathArgs("translation1", "path1", 2); messages2.Count.Should().Be(1); messages2.ElementAt(0).Should().Be("message2"); var messages3 = messageCache.GetMessagesWithPathArgs("translation1", "path2", 1); messages3.Count.Should().Be(1); messages3.ElementAt(0).Should().Be("message3"); var messages4 = messageCache.GetMessagesWithPathArgs("translation2", "path1", 1); messages4.Count.Should().Be(1); messages4.ElementAt(0).Should().Be("message4"); } [Fact] public void Should_AddMessageWithPathArgs_SingleError() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message" }); var messages = messageCache.GetMessagesWithPathArgs("translation", "path", 1); messages.Count.Should().Be(1); messages.ElementAt(0).Should().Be("message"); } [Fact] public void Should_ThrowException_When_AddMultipleTimesToSameError() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => { messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message2" }); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullMessages() { var messageCache = new MessageCache(); Action action = () => { messageCache.AddMessageWithPathArgs("translation", null, 1, null); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_MessagesWithNull() { var messageCache = new MessageCache(); Action action = () => { messageCache.AddMessageWithPathArgs("translation", null, 1, new[] { "message", null }); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullPath() { var messageCache = new MessageCache(); Action action = () => { messageCache.AddMessageWithPathArgs("translation", null, 1, new[] { "message" }); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslationName() { var messageCache = new MessageCache(); Action action = () => { messageCache.AddMessageWithPathArgs(null, "path", 1, new[] { "message" }); }; action.Should().ThrowExactly(); } } public class GetMessagesWithPathArgs { [Fact] public void Should_GetMessagesWithPathArgs() { var messageCache = new MessageCache(); var messages = new[] { "message1", "message2" }; messageCache.AddMessageWithPathArgs("translation", "path", 1, messages); var cachedMessages = messageCache.GetMessagesWithPathArgs("translation", "path", 1); cachedMessages.Should().BeSameAs(messages); cachedMessages.Count.Should().Be(2); cachedMessages.ElementAt(0).Should().Be("message1"); cachedMessages.ElementAt(1).Should().Be("message2"); } [Fact] public void Should_ThrowException_When_NullPath() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.GetMessagesWithPathArgs("translation", null, 1); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslation() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.GetMessagesWithPathArgs(null, "path", 1); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidErrorId() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.GetMessagesWithPathArgs("translation", "path", 100); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidPath() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.GetMessagesWithPathArgs("translation", "invalidPath", 1); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidTranslation() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.GetMessagesWithPathArgs("invalidTranslation", "path", 1); action.Should().ThrowExactly(); } } public class IsMessageWithPathArgsCached { [Fact] public void Should_ThrowException_When_NullPath() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.IsMessageWithPathArgsCached("translation", null, 1); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslation() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); Action action = () => messageCache.IsMessageWithPathArgsCached(null, "path", 1); action.Should().ThrowExactly(); } [Fact] public void Should_ReturnTrue_When_MessageIsCached() { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); messageCache.AddMessageWithPathArgs("translation1", "path", 1, new[] { "message1" }); var result1 = messageCache.IsMessageWithPathArgsCached("translation", "path", 1); result1.Should().Be(true); var result2 = messageCache.IsMessageWithPathArgsCached("translation1", "path", 1); result2.Should().Be(true); } [Theory] [InlineData("translationName", "path", 2)] [InlineData("translationName", "invalidPath", 1)] [InlineData("invalidTranslationName", "path", 1)] public void Should_ReturnFalse_When_MessageIsNotCached(string translationName, string path, int errorId) { var messageCache = new MessageCache(); messageCache.AddMessageWithPathArgs("translation", "path", 1, new[] { "message1" }); var result = messageCache.IsMessageWithPathArgsCached(translationName, path, errorId); result.Should().Be(false); } } public class AddIndexedPathPlaceholders { [Fact] public void Should_AddIndexedPathPlaceholders() { var messageCache = new MessageCache(); messageCache.AddIndexedPathPlaceholders("translationName", 1, new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }); var result = messageCache.GetIndexedPathPlaceholders("translationName", 1); result.Keys.Count().Should().Be(1); result.Keys.Single().Should().Be(1); result[1].Count.Should().Be(1); result[1].Single().Name.Should().Be("arg"); result[1].Single().Placeholder.Should().Be("{arg|test=true}"); result[1].Single().Parameters.Keys.Count().Should().Be(1); result[1].Single().Parameters.Keys.Single().Should().Be("test"); result[1].Single().Parameters["test"].Should().Be("true"); } [Fact] public void Should_AddIndexedPathPlaceholders_Multiple() { var messageCache = new MessageCache(); messageCache.AddIndexedPathPlaceholders("translationName1", 1, new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg1", Placeholder = "{arg1|test1=true1}", Parameters = new Dictionary { ["test1"] = "true1" } } } }); messageCache.AddIndexedPathPlaceholders("translationName1", 2, new Dictionary>() { [2] = new[] { new ArgPlaceholder() { Name = "arg2", Placeholder = "{arg2|test2=true2}", Parameters = new Dictionary { ["test2"] = "true2" } } } }); messageCache.AddIndexedPathPlaceholders("translationName2", 1, new Dictionary>() { [3] = new[] { new ArgPlaceholder() { Name = "arg3", Placeholder = "{arg3|test3=true3}", Parameters = new Dictionary { ["test3"] = "true3" } } } }); var result1 = messageCache.GetIndexedPathPlaceholders("translationName1", 1); result1.Keys.Count().Should().Be(1); result1.Keys.Single().Should().Be(1); result1[1].Count.Should().Be(1); result1[1].Single().Name.Should().Be("arg1"); result1[1].Single().Placeholder.Should().Be("{arg1|test1=true1}"); result1[1].Single().Parameters.Keys.Count().Should().Be(1); result1[1].Single().Parameters.Keys.Single().Should().Be("test1"); result1[1].Single().Parameters["test1"].Should().Be("true1"); var result2 = messageCache.GetIndexedPathPlaceholders("translationName1", 2); result2.Keys.Count().Should().Be(1); result2.Keys.Single().Should().Be(2); result2[2].Count.Should().Be(1); result2[2].Single().Name.Should().Be("arg2"); result2[2].Single().Placeholder.Should().Be("{arg2|test2=true2}"); result2[2].Single().Parameters.Keys.Count().Should().Be(1); result2[2].Single().Parameters.Keys.Single().Should().Be("test2"); result2[2].Single().Parameters["test2"].Should().Be("true2"); var result3 = messageCache.GetIndexedPathPlaceholders("translationName2", 1); result3.Keys.Count().Should().Be(1); result3.Keys.Single().Should().Be(3); result3[3].Count.Should().Be(1); result3[3].Single().Name.Should().Be("arg3"); result3[3].Single().Placeholder.Should().Be("{arg3|test3=true3}"); result3[3].Single().Parameters.Keys.Count().Should().Be(1); result3[3].Single().Parameters.Keys.Single().Should().Be("test3"); result3[3].Single().Parameters["test3"].Should().Be("true3"); } [Fact] public void Should_ThrowException_AddingMultiple_ToSameTranslationAndError() { var messageCache = new MessageCache(); messageCache.AddIndexedPathPlaceholders("translationName1", 1, new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg1", Placeholder = "{arg1|test1=true1}", Parameters = new Dictionary { ["test1"] = "true1" } } } }); Action action = () => messageCache.AddIndexedPathPlaceholders("translationName1", 1, new Dictionary>() { [2] = new[] { new ArgPlaceholder() { Name = "arg2", Placeholder = "{arg2|test2=true2}", Parameters = new Dictionary { ["test2"] = "true2" } } } }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslation() { var messageCache = new MessageCache(); Action action = () => messageCache.AddIndexedPathPlaceholders(null, 1, new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullIndexedPlaceholders() { var messageCache = new MessageCache(); Action action = () => messageCache.AddIndexedPathPlaceholders("translation", 1, null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_IndexedPlaceholders_WithNullPlaceholdersArray() { var messageCache = new MessageCache(); Action action = () => messageCache.AddIndexedPathPlaceholders("translation", 1, new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } }, [2] = null }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_IndexedPlaceholders_WithNullPlaceholderInArray() { var messageCache = new MessageCache(); Action action = () => messageCache.AddIndexedPathPlaceholders("translation", 1, new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } }, null }, }); action.Should().ThrowExactly(); } public static IEnumerable Should_ThrowException_When_IndexedPlaceholders_WithPlaceholder_WithNullValue_Data() { yield return new object[] { new ArgPlaceholder(), }; yield return new object[] { new ArgPlaceholder() { Name = null, Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } }, }; yield return new object[] { new ArgPlaceholder() { Name = "arg", Placeholder = null, Parameters = new Dictionary { ["test"] = "true" } }, }; yield return new object[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = null }, }; yield return new object[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = null } }, }; } [Theory] [MemberData(nameof(Should_ThrowException_When_IndexedPlaceholders_WithPlaceholder_WithNullValue_Data))] public void Should_ThrowException_When_IndexedPlaceholders_WithPlaceholder_WithNullValue(ArgPlaceholder argPlaceholder) { var messageCache = new MessageCache(); Action action = () => messageCache.AddIndexedPathPlaceholders("translation", 1, new Dictionary>() { [1] = new[] { argPlaceholder }, }); action.Should().ThrowExactly(); } } public class GetIndexedPathPlaceholders { [Fact] public void Should_GetIndexedPathPlaceholders() { var messageCache = new MessageCache(); var indexedPlaceholders = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; messageCache.AddIndexedPathPlaceholders("translationName", 1, indexedPlaceholders); var result = messageCache.GetIndexedPathPlaceholders("translationName", 1); result.Should().BeSameAs(indexedPlaceholders); } [Fact] public void Should_GetIndexedPathPlaceholders_DifferentTranslationNameAndError() { var messageCache = new MessageCache(); var indexedPlaceholders1 = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; var indexedPlaceholders2 = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; var indexedPlaceholders3 = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; messageCache.AddIndexedPathPlaceholders("translationName1", 1, indexedPlaceholders1); messageCache.AddIndexedPathPlaceholders("translationName1", 2, indexedPlaceholders2); messageCache.AddIndexedPathPlaceholders("translationName2", 1, indexedPlaceholders3); var result1 = messageCache.GetIndexedPathPlaceholders("translationName1", 1); result1.Should().BeSameAs(indexedPlaceholders1); var result2 = messageCache.GetIndexedPathPlaceholders("translationName1", 2); result2.Should().BeSameAs(indexedPlaceholders2); var result3 = messageCache.GetIndexedPathPlaceholders("translationName2", 1); result3.Should().BeSameAs(indexedPlaceholders3); } [Fact] public void Should_ThrowException_When_InvalidErrorId() { var messageCache = new MessageCache(); var indexedPlaceholders = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; messageCache.AddIndexedPathPlaceholders("translationName", 1, indexedPlaceholders); Action action = () => messageCache.GetIndexedPathPlaceholders("translationName", 2); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidTranslationName() { var messageCache = new MessageCache(); var indexedPlaceholders = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; messageCache.AddIndexedPathPlaceholders("translationName", 1, indexedPlaceholders); Action action = () => messageCache.GetIndexedPathPlaceholders("invalidTranslationName", 1); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslationName() { var messageCache = new MessageCache(); var indexedPlaceholders = new Dictionary>() { [1] = new[] { new ArgPlaceholder() { Name = "arg", Placeholder = "{arg|test=true}", Parameters = new Dictionary { ["test"] = "true" } } } }; messageCache.AddIndexedPathPlaceholders("translationName", 1, indexedPlaceholders); Action action = () => messageCache.GetIndexedPathPlaceholders(null, 1); action.Should().ThrowExactly(); } } public class VerifyIntegrity { [Fact] public void Should_BeValid_When_EmptyCache() { var messageCache = new MessageCache(); messageCache.VerifyIntegrity(); } [Fact] public void Should_BeValid_When_SingleError() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message1", "message2" }); messageCache.VerifyIntegrity(); } [Fact] public void Should_BeValid_When_MultipleErrors() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22" }); messageCache.AddMessage("translation1", 3, new[] { "message31", "message32" }); messageCache.VerifyIntegrity(); } [Fact] public void Should_ThrowException_When_MultipleErrors_DifferentAmountOfMessages() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22" }); messageCache.AddMessage("translation2", 1, new[] { "message31", "message32" }); messageCache.AddMessage("translation2", 2, new[] { "message41", "message42", "message43" }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"ErrorId 2, messages amount is expected to be 2 but found 3 in translation `translation2`"); } [Fact] public void Should_ThrowException_When_ErrorIdNotInAllTranslations() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation2", 1, new[] { "message21", "message22" }); messageCache.AddMessage("translation2", 2, new[] { "message31", "message32" }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"ErrorId 2 is not present in all translations"); } [Fact] public void Should_BeValid_When_PathPlaceholders() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation2", 1, new[] { "message21", "message22" }); messageCache.AddIndexedPathPlaceholders("translation1", 1, new Dictionary>() { [0] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation2", 1, new Dictionary>() { [1] = Array.Empty(), }); messageCache.VerifyIntegrity(); } [Fact] public void Should_ThrowException_When_PathPlaceholders_WithInvalidTranslation() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation2", 1, new[] { "message21", "message22" }); messageCache.AddIndexedPathPlaceholders("translation1", 1, new Dictionary>() { [0] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation3", 1, new Dictionary>() { [1] = Array.Empty(), }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"Translation `translation3` is not expected in path placeholders"); } [Fact] public void Should_ThrowException_When_PathPlaceholders_WithInvalidErrorId() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation2", 1, new[] { "message21", "message22" }); messageCache.AddIndexedPathPlaceholders("translation1", 1, new Dictionary>() { [0] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation2", 2, new Dictionary>() { [1] = Array.Empty(), }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"ErrorId 2 is not expected in path placeholders (translation `translation2`)"); } [Fact] public void Should_ThrowException_When_PathPlaceholders_IndexExceedsMessagesAmount() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22" }); messageCache.AddIndexedPathPlaceholders("translation1", 1, new Dictionary>() { [0] = Array.Empty(), [1] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation1", 2, new Dictionary>() { [0] = Array.Empty(), [3] = Array.Empty(), }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"ErrorId 2 max index for path placeholder is 1, but found 3 (translation `translation1`)"); } [Fact] public void Should_ThrowException_When_MessageWithPathArgs_WithInvalidTranslation() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22" }); messageCache.AddMessageWithPathArgs("translation1", "path", 1, new[] { "message11", "message12" }); messageCache.AddMessageWithPathArgs("translation2", "path", 1, new[] { "message11", "message22" }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"Translation `translation2` is not expected in messages with path args"); } [Fact] public void Should_ThrowException_When_MessageWithPathArgs_WithInvalidErrorId() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22" }); messageCache.AddMessageWithPathArgs("translation1", "path", 1, new[] { "message11", "message12" }); messageCache.AddMessageWithPathArgs("translation1", "path", 3, new[] { "message11", "message22" }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"Error ID 3 in translation `translation1` is not expected in messages with path args"); } [Fact] public void Should_ThrowException_When_MessageWithPathArgs_MaxMessagesAmountExceeded() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22" }); messageCache.AddMessageWithPathArgs("translation1", "path1", 1, new[] { "message11", "message12" }); messageCache.AddMessageWithPathArgs("translation1", "path2", 2, new[] { "message21", "message22", "message23" }); Action action = () => messageCache.VerifyIntegrity(); action.Should().ThrowExactly().WithMessage($"Error ID 2 is expected to have max 2 messages, but found 3 in messages with path args (for translation `translation1` and path `path2`)"); } [Fact] public void Should_BeValid_When_MessageWithPathArgs() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22", "message22" }); messageCache.AddMessageWithPathArgs("translation1", "path1", 1, new[] { "message11", "message12" }); messageCache.AddMessageWithPathArgs("translation1", "path2", 2, new[] { "message21", "message22", "message23" }); messageCache.VerifyIntegrity(); } [Fact] public void Should_BeValid_When_Placeholders_And_MessageWithPathArgs() { var messageCache = new MessageCache(); messageCache.AddMessage("translation1", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation1", 2, new[] { "message21", "message22", "message22" }); messageCache.AddMessage("translation2", 1, new[] { "message11", "message12" }); messageCache.AddMessage("translation2", 2, new[] { "message21", "message22", "message22" }); messageCache.AddIndexedPathPlaceholders("translation1", 1, new Dictionary>() { [0] = Array.Empty(), [1] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation1", 2, new Dictionary>() { [0] = Array.Empty(), [1] = Array.Empty(), [2] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation2", 1, new Dictionary>() { [1] = Array.Empty(), }); messageCache.AddIndexedPathPlaceholders("translation2", 2, new Dictionary>() { [0] = Array.Empty(), [2] = Array.Empty(), }); messageCache.AddMessageWithPathArgs("translation1", "path1", 1, new[] { "message11", "message12" }); messageCache.AddMessageWithPathArgs("translation1", "path2", 2, new[] { "message21", "message22", "message23" }); messageCache.AddMessageWithPathArgs("translation2", "path1", 1, new[] { "message12" }); messageCache.AddMessageWithPathArgs("translation2", "path2", 2, new[] { "message21", "message23" }); messageCache.VerifyIntegrity(); } } [Fact] public void Should_Initialize() { _ = new MessageCache(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/MessageServiceTests.cs ================================================ namespace Validot.Tests.Unit.Errors { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Errors; using Validot.Errors.Args; using Xunit; public class MessageServiceTests { public static readonly Dictionary> DefaultTranslations = new Dictionary>() { ["translation1"] = new Dictionary() { ["key11"] = "message11", ["key12"] = "message12", ["key21"] = "message21 {numberArg} {textArg}", ["key22"] = "message22 {textArg|case=lower} {textArg|case=upper}" }, ["translation2"] = new Dictionary() { ["key11"] = "MESSAGE_11", ["key12"] = "MESSAGE_12", ["key21"] = "MESSAGE_21 {numberArg|format=0000} {textArg|case=upper}", ["key22"] = "MESSAGE_22 {textArg} {textArg}" } }; public static readonly Dictionary DefaultErrors = new Dictionary() { [0] = new Error() { Messages = new[] { "key11", "key12" }, Args = Array.Empty() }, [1] = new Error() { Messages = new[] { "key21", "key22" }, Args = new[] { Arg.Number("numberArg", 123), Arg.Text("textArg", "textArgValue") } }, [2] = new Error() { Messages = Array.Empty(), Codes = new[] { "NO_MESSAGES_CODE" }, Args = Array.Empty() } }; public static readonly Dictionary> DefaultTemplate = new Dictionary>() { ["name"] = new[] { 0 }, ["path.name"] = new[] { 1 }, ["new.path.name"] = new[] { 0, 1 }, ["codes.only"] = new[] { 2 }, ["codes.mix"] = new[] { 0, 1, 2 }, }; [Fact] public void Should_Initialize() { _ = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); } [Fact] public void Should_Initialize_WithEmptyTranslations() { _ = new MessageService(new Dictionary>(), DefaultErrors, DefaultTemplate); } [Fact] public void Should_ThrowException_When_Initialize_With_Translations_Null() { Action action = () => new MessageService(null, DefaultErrors, DefaultTemplate); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_Translations_WithNull() { var translations1 = new Dictionary>() { ["translation1"] = DefaultTranslations["translation2"], ["translation2"] = null }; var translations2 = new Dictionary>() { ["translation1"] = DefaultTranslations["translation2"], ["translation2"] = new Dictionary() { ["key11"] = null } }; Action action1 = () => new MessageService(translations1, DefaultErrors, DefaultTemplate); Action action2 = () => new MessageService(translations2, DefaultErrors, DefaultTemplate); action1.Should().ThrowExactly(); action2.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_Errors_Null() { Action action = () => new MessageService(DefaultTranslations, null, DefaultTemplate); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_Errors_WithNull() { var errors0 = new Dictionary() { [0] = null, [1] = DefaultErrors[1] }; var errors1 = new Dictionary() { [0] = new Error() { Messages = null, Args = Array.Empty() }, [1] = DefaultErrors[1] }; var errors2 = new Dictionary() { [0] = new Error() { Messages = new[] { "message", null }, Args = Array.Empty() }, [1] = DefaultErrors[1] }; var errors3 = new Dictionary() { [0] = new Error() { Messages = new[] { "message", null }, Args = null }, [1] = DefaultErrors[1] }; var errors4 = new Dictionary() { [0] = new Error() { Messages = new[] { "message", null }, Args = new IArg[] { Arg.Text("a", "a"), null } }, [1] = DefaultErrors[1] }; Action action0 = () => new MessageService(DefaultTranslations, errors0, DefaultTemplate); Action action1 = () => new MessageService(DefaultTranslations, errors1, DefaultTemplate); Action action2 = () => new MessageService(DefaultTranslations, errors2, DefaultTemplate); Action action3 = () => new MessageService(DefaultTranslations, errors3, DefaultTemplate); Action action4 = () => new MessageService(DefaultTranslations, errors4, DefaultTemplate); action0.Should().ThrowExactly(); action1.Should().ThrowExactly(); action2.Should().ThrowExactly(); action3.Should().ThrowExactly(); action4.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_Template_Null() { Action action = () => new MessageService(DefaultTranslations, DefaultErrors, null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_Template_WithNull() { var template = new Dictionary>() { ["name"] = new[] { 0 }, ["path.name"] = new[] { 1 }, ["new.path.name"] = null }; Action action = () => new MessageService(DefaultTranslations, DefaultErrors, template); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_Template_WithInvalidErrorId() { var template = new Dictionary>() { ["name"] = new[] { 0 }, ["path.name"] = new[] { 666 } }; Action action = () => new MessageService(DefaultTranslations, DefaultErrors, template); action.Should().ThrowExactly(); } [Fact] public void Should_Load_TranslationNames() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); messageService.TranslationNames.Should().NotBeNull(); messageService.TranslationNames.Count.Should().Be(2); messageService.TranslationNames.Should().Contain("translation1"); messageService.TranslationNames.Should().Contain("translation2"); } public class GetTranslation { [Fact] public void Should_GetTranslation() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var translation1 = messageService.GetTranslation("translation1"); translation1.Should().BeSameAs(DefaultTranslations["translation1"]); translation1["key11"].Should().Be("message11"); translation1["key12"].Should().Be("message12"); translation1["key21"].Should().Be("message21 {numberArg} {textArg}"); translation1["key22"].Should().Be("message22 {textArg|case=lower} {textArg|case=upper}"); var translation2 = messageService.GetTranslation("translation2"); translation2.Should().BeSameAs(DefaultTranslations["translation2"]); translation2["key11"].Should().Be("MESSAGE_11"); translation2["key12"].Should().Be("MESSAGE_12"); translation2["key21"].Should().Be("MESSAGE_21 {numberArg|format=0000} {textArg|case=upper}"); translation2["key22"].Should().Be("MESSAGE_22 {textArg} {textArg}"); } [Fact] public void Should_ThrowException_When_InvalidTranslationName() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); Action action1 = () => messageService.GetTranslation("translationX"); Action action2 = () => messageService.GetTranslation("TRANSLATION1"); action1.Should().ThrowExactly(); action2.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullTranslationName() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); Action action = () => messageService.GetTranslation(null); action.Should().ThrowExactly(); } } public class GetMessages { [Fact] public void Should_ThrowException_When_Errors() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); Action action = () => messageService.GetMessages(null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Errors_WithNullErrorsIds() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); Action action = () => messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["path.name"] = null }, "translation1"); action.Should().ThrowExactly(); } [Theory] [InlineData(null)] [InlineData("translation1")] [InlineData("translation2")] public void Should_Get_EmptyDictionary_When_PathsWithoutMessages(string translationName) { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["codes.only"] = new List() { 2 } }, translationName); result.Should().BeEmpty(); } [Theory] [InlineData(null)] [InlineData("translation1")] [InlineData("translation2")] public void Should_Get_EmptyDictionary_When_ErrorsWithoutMessages(string translationName) { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["codes.mix"] = new List() { 2 } }, translationName); result.Should().BeEmpty(); } [Theory] [InlineData(null)] [InlineData("translation1")] [InlineData("translation2")] public void Should_Get_EmptyDictionary_When_OutputWithoutMessages(string translationName) { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["codes.only"] = new List() { 2 }, ["codes.mix"] = new List() { 2 } }, translationName); result.Should().BeEmpty(); } [Fact] public void Should_Get_From_SinglePath_When_SingleError() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["path.name"] = new List() { 1 } }, "translation2"); result.Count.Should().Be(1); result.Keys.Should().Contain("path.name"); result["path.name"].Count.Should().Be(2); result["path.name"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["path.name"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_From_SinglePath_When_ManyErrors() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0, 1 } }, "translation2"); result.Count.Should().Be(1); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(4); result["new.path.name"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); result["new.path.name"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.path.name"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_From_SinglePathWithManyErrors_But_SingleError() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0 } }, "translation2"); result.Count.Should().Be(1); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(2); result["new.path.name"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); } [Fact] public void Should_Get_From_ManyPaths() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["path.name"] = new List() { 1 }, ["new.path.name"] = new List() { 0, 1 } }, "translation2"); result.Count.Should().Be(3); result.Keys.Should().Contain("name"); result["name"].Count.Should().Be(2); result["name"].ElementAt(0).Should().Be("MESSAGE_11"); result["name"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("path.name"); result["path.name"].Count.Should().Be(2); result["path.name"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["path.name"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(4); result["new.path.name"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); result["new.path.name"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.path.name"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_From_ManyPaths_SkippingPathsWithNoMessages() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["path.name"] = new List() { 1 }, ["new.path.name"] = new List() { 0, 1 }, ["codes.only"] = new List() { 2 } }, "translation2"); result.Count.Should().Be(3); result.Keys.Should().Contain("name"); result["name"].Count.Should().Be(2); result["name"].ElementAt(0).Should().Be("MESSAGE_11"); result["name"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("path.name"); result["path.name"].Count.Should().Be(2); result["path.name"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["path.name"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(4); result["new.path.name"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); result["new.path.name"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.path.name"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_From_ManyPaths_SkippingErrorsWithNoMessages() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["path.name"] = new List() { 1 }, ["new.path.name"] = new List() { 0, 1 }, ["codes.only"] = new List() { 2 }, ["codes.mix"] = new List() { 0, 1, 2 } }, "translation2"); result.Count.Should().Be(4); result.Keys.Should().Contain("name"); result["name"].Count.Should().Be(2); result["name"].ElementAt(0).Should().Be("MESSAGE_11"); result["name"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("path.name"); result["path.name"].Count.Should().Be(2); result["path.name"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["path.name"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(4); result["new.path.name"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); result["new.path.name"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.path.name"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("codes.mix"); result["new.path.name"].Count.Should().Be(4); result["new.path.name"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); result["new.path.name"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.path.name"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_From_ManyPaths_And_DifferentTranslations() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result1 = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["new.path.name"] = new List() { 1 } }, "translation1"); result1.Count.Should().Be(2); result1.Keys.Should().Contain("name"); result1["name"].Count.Should().Be(2); result1["name"].ElementAt(0).Should().Be("message11"); result1["name"].ElementAt(1).Should().Be("message12"); result1.Keys.Should().Contain("new.path.name"); result1["new.path.name"].Count.Should().Be(2); result1["new.path.name"].ElementAt(0).Should().Be("message21 123 textArgValue"); result1["new.path.name"].ElementAt(1).Should().Be("message22 textargvalue TEXTARGVALUE"); var result2 = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["new.path.name"] = new List() { 1 } }, "translation2"); result2.Count.Should().Be(2); result2.Keys.Should().Contain("name"); result2["name"].Count.Should().Be(2); result2["name"].ElementAt(0).Should().Be("MESSAGE_11"); result2["name"].ElementAt(1).Should().Be("MESSAGE_12"); result2.Keys.Should().Contain("new.path.name"); result2["new.path.name"].Count.Should().Be(2); result2["new.path.name"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result2["new.path.name"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_With_EnglishTranslation_When_NoTranslationSpecified() { var translations = new Dictionary>() { ["English"] = new Dictionary() { ["key11"] = "en11", ["key12"] = "en12", ["key21"] = "en21 {numberArg} {textArg|case=upper}", ["key22"] = "en22 {textArg}" }, ["translation1"] = DefaultTranslations["translation1"], ["translation2"] = DefaultTranslations["translation2"] }; var messageService = new MessageService(translations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages(new Dictionary>() { ["path.name"] = new List() { 1 } }); result.Count.Should().Be(1); result.Keys.Should().Contain("path.name"); result["path.name"].Count.Should().Be(2); result["path.name"].ElementAt(0).Should().Be("en21 123 TEXTARGVALUE"); result["path.name"].ElementAt(1).Should().Be("en22 textArgValue"); } [Fact] public void Should_Get_With_ErrorsWithCustomMessages() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "customMessage", "key12" }, Args = Array.Empty() }, [1] = DefaultErrors[1], [2] = DefaultErrors[2] }; var messageService = new MessageService(DefaultTranslations, errors, DefaultTemplate); var result1 = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0 }, }, "translation1"); result1.Count.Should().Be(1); result1.Keys.Should().Contain("new.path.name"); result1["new.path.name"].Count.Should().Be(2); result1["new.path.name"].ElementAt(0).Should().Be("customMessage"); result1["new.path.name"].ElementAt(1).Should().Be("message12"); var result2 = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0 }, }, "translation2"); result2.Count.Should().Be(1); result2.Keys.Should().Contain("new.path.name"); result2["new.path.name"].Count.Should().Be(2); result2["new.path.name"].ElementAt(0).Should().Be("customMessage"); result2["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); } [Fact] public void Should_Get_With_TranslateArg() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "translation {_translation|key=key12}", "key12" }, Args = Array.Empty() }, [1] = DefaultErrors[1], [2] = DefaultErrors[2] }; var messageService = new MessageService(DefaultTranslations, errors, DefaultTemplate); var result1 = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0 }, }, "translation1"); result1.Count.Should().Be(1); result1.Keys.Should().Contain("new.path.name"); result1["new.path.name"].Count.Should().Be(2); result1["new.path.name"].ElementAt(0).Should().Be("translation message12"); result1["new.path.name"].ElementAt(1).Should().Be("message12"); var result2 = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0 }, }, "translation2"); result2.Count.Should().Be(1); result2.Keys.Should().Contain("new.path.name"); result2["new.path.name"].Count.Should().Be(2); result2["new.path.name"].ElementAt(0).Should().Be("translation MESSAGE_12"); result2["new.path.name"].ElementAt(1).Should().Be("MESSAGE_12"); } [Fact] public void Should_Get_With_TranslateArg_And_LeaveKey_When_KeyIsInvalid() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "translation {_translation|key=invalidKey}", "key12" }, Args = Array.Empty() }, [1] = DefaultErrors[1], [2] = DefaultErrors[2] }; var messageService = new MessageService(DefaultTranslations, errors, DefaultTemplate); var result1 = messageService.GetMessages( new Dictionary>() { ["new.path.name"] = new List() { 0 }, }, "translation1"); result1.Count.Should().Be(1); result1.Keys.Should().Contain("new.path.name"); result1["new.path.name"].Count.Should().Be(2); result1["new.path.name"].ElementAt(0).Should().Be("translation invalidKey"); result1["new.path.name"].ElementAt(1).Should().Be("message12"); } [Fact] public void Should_Get_With_PathArgs() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "name11: {_name}", "path12: {_path}" }, Args = Array.Empty() }, [1] = new Error() { Messages = new[] { "name21: {_name}", "path22: {_path}" }, Args = Array.Empty() }, [2] = DefaultErrors[2] }; var messageService = new MessageService(DefaultTranslations, errors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["path.name"] = new List() { 1 }, ["new.path.name"] = new List() { 0, 1 }, }, "translation1"); result.Count.Should().Be(3); result.Keys.Should().Contain("name"); result["name"].Count.Should().Be(2); result["name"].ElementAt(0).Should().Be("name11: name"); result["name"].ElementAt(1).Should().Be("path12: name"); result.Keys.Should().Contain("path.name"); result["path.name"].Count.Should().Be(2); result["path.name"].ElementAt(0).Should().Be("name21: name"); result["path.name"].ElementAt(1).Should().Be("path22: path.name"); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(4); result["new.path.name"].ElementAt(0).Should().Be("name11: name"); result["new.path.name"].ElementAt(1).Should().Be("path12: new.path.name"); result["new.path.name"].ElementAt(2).Should().Be("name21: name"); result["new.path.name"].ElementAt(3).Should().Be("path22: new.path.name"); } [Fact] public void Should_Get_With_PathArgs_MixedWithOtherArgs() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "name11: {_name} {_translation|key=key11} {numberArg}", "path12: {_path} {textArg|case=upper}" }, Args = new[] { Arg.Text("textArg", "textArgValue"), Arg.Number("numberArg", 123) } }, [1] = DefaultErrors[1], [2] = DefaultErrors[2] }; var messageService = new MessageService(DefaultTranslations, errors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["name"] = new List() { 0 }, ["new.path.name"] = new List() { 0, }, }, "translation2"); result.Count.Should().Be(2); result.Keys.Should().Contain("name"); result["name"].Count.Should().Be(2); result["name"].ElementAt(0).Should().Be("name11: name MESSAGE_11 123"); result["name"].ElementAt(1).Should().Be("path12: name TEXTARGVALUE"); result.Keys.Should().Contain("new.path.name"); result["new.path.name"].Count.Should().Be(2); result["new.path.name"].ElementAt(0).Should().Be("name11: name MESSAGE_11 123"); result["new.path.name"].ElementAt(1).Should().Be("path12: new.path.name TEXTARGVALUE"); } [Fact] public void Should_Get_With_IndexesInPath() { var template = new Dictionary>() { ["#"] = new[] { 0 }, ["path.#.name"] = new[] { 1 }, ["new.#.path.#"] = new[] { 0, 1 }, }; var messageService = new MessageService(DefaultTranslations, DefaultErrors, template); var result = messageService.GetMessages( new Dictionary>() { ["#1"] = new List() { 0 }, ["path.#1.name"] = new List() { 1, }, ["new.#4.path.#9"] = new List() { 1, }, }, "translation2"); result.Count.Should().Be(3); result.Keys.Should().Contain("#1"); result["#1"].Count.Should().Be(2); result["#1"].ElementAt(0).Should().Be("MESSAGE_11"); result["#1"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("path.#1.name"); result["path.#1.name"].Count.Should().Be(2); result["path.#1.name"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["path.#1.name"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("new.#4.path.#9"); result["new.#4.path.#9"].Count.Should().Be(2); result["new.#4.path.#9"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.#4.path.#9"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_With_IndexesInPath_With_SamePathWithDifferentIndexes() { var template = new Dictionary>() { ["#"] = new[] { 0, 1 }, ["new.#.path.#"] = new[] { 0, 1 }, }; var messageService = new MessageService(DefaultTranslations, DefaultErrors, template); var result = messageService.GetMessages( new Dictionary>() { ["#1"] = new List() { 0 }, ["#99"] = new List() { 1 }, ["new.#0.path.#99"] = new List() { 0, }, ["new.#10.path.#8"] = new List() { 1, }, ["new.#21.path.#8888"] = new List() { 0, 1, }, }, "translation2"); result.Keys.Count().Should().Be(5); result.Keys.Should().Contain("#1"); result["#1"].Count.Should().Be(2); result["#1"].ElementAt(0).Should().Be("MESSAGE_11"); result["#1"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("#99"); result["#99"].Count.Should().Be(2); result["#99"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["#99"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("new.#0.path.#99"); result["new.#0.path.#99"].Count.Should().Be(2); result["new.#0.path.#99"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.#0.path.#99"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("new.#10.path.#8"); result["new.#10.path.#8"].Count.Should().Be(2); result["new.#10.path.#8"].ElementAt(0).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.#10.path.#8"].ElementAt(1).Should().Be("MESSAGE_22 textArgValue textArgValue"); result.Keys.Should().Contain("new.#21.path.#8888"); result["new.#21.path.#8888"].Count.Should().Be(4); result["new.#21.path.#8888"].ElementAt(0).Should().Be("MESSAGE_11"); result["new.#21.path.#8888"].ElementAt(1).Should().Be("MESSAGE_12"); result["new.#21.path.#8888"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["new.#21.path.#8888"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_With_IndexesInPath_With_PathArgs() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "name11: {_name}", "path12: {_path}" }, Args = Array.Empty() }, [1] = new Error() { Messages = new[] { "name21: {_name}", "path22: {_path}" }, Args = Array.Empty() } }; var template = new Dictionary>() { ["#"] = new[] { 0, 1 }, ["new.#.path.#"] = new[] { 0, 1 }, }; var messageService = new MessageService(DefaultTranslations, errors, template); var result = messageService.GetMessages( new Dictionary>() { ["#1"] = new List() { 0 }, ["#99"] = new List() { 1 }, ["new.#0.path.#99"] = new List() { 0, }, ["new.#10.path.#8"] = new List() { 1, }, ["new.#21.path.#8888"] = new List() { 0, 1, }, }, "translation2"); result.Keys.Count().Should().Be(5); result.Keys.Should().Contain("#1"); result["#1"].Count.Should().Be(2); result["#1"].ElementAt(0).Should().Be("name11: #1"); result["#1"].ElementAt(1).Should().Be("path12: #1"); result.Keys.Should().Contain("#99"); result["#99"].Count.Should().Be(2); result["#99"].ElementAt(0).Should().Be("name21: #99"); result["#99"].ElementAt(1).Should().Be("path22: #99"); result.Keys.Should().Contain("new.#0.path.#99"); result["new.#0.path.#99"].Count.Should().Be(2); result["new.#0.path.#99"].ElementAt(0).Should().Be("name11: #99"); result["new.#0.path.#99"].ElementAt(1).Should().Be("path12: new.#0.path.#99"); result.Keys.Should().Contain("new.#10.path.#8"); result["new.#10.path.#8"].Count.Should().Be(2); result["new.#10.path.#8"].ElementAt(0).Should().Be("name21: #8"); result["new.#10.path.#8"].ElementAt(1).Should().Be("path22: new.#10.path.#8"); result.Keys.Should().Contain("new.#21.path.#8888"); result["new.#21.path.#8888"].Count.Should().Be(4); result["new.#21.path.#8888"].ElementAt(0).Should().Be("name11: #8888"); result["new.#21.path.#8888"].ElementAt(1).Should().Be("path12: new.#21.path.#8888"); result["new.#21.path.#8888"].ElementAt(2).Should().Be("name21: #8888"); result["new.#21.path.#8888"].ElementAt(3).Should().Be("path22: new.#21.path.#8888"); } [Fact] public void Should_Get_With_NotRegisteredPaths() { var messageService = new MessageService(DefaultTranslations, DefaultErrors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["some.not.registered.path"] = new List() { 0 }, ["another.path.#2.with.#12.indexes"] = new List() { 0, 1 }, }, "translation2"); result.Count.Should().Be(2); result.Keys.Should().Contain("some.not.registered.path"); result["some.not.registered.path"].Count.Should().Be(2); result["some.not.registered.path"].ElementAt(0).Should().Be("MESSAGE_11"); result["some.not.registered.path"].ElementAt(1).Should().Be("MESSAGE_12"); result.Keys.Should().Contain("another.path.#2.with.#12.indexes"); result["another.path.#2.with.#12.indexes"].Count.Should().Be(4); result["another.path.#2.with.#12.indexes"].ElementAt(0).Should().Be("MESSAGE_11"); result["another.path.#2.with.#12.indexes"].ElementAt(1).Should().Be("MESSAGE_12"); result["another.path.#2.with.#12.indexes"].ElementAt(2).Should().Be("MESSAGE_21 0123 TEXTARGVALUE"); result["another.path.#2.with.#12.indexes"].ElementAt(3).Should().Be("MESSAGE_22 textArgValue textArgValue"); } [Fact] public void Should_Get_With_NotRegisteredPaths_With_PathArgs() { var errors = new Dictionary() { [0] = new Error() { Messages = new[] { "name11: {_name}", "path12: {_path}" }, Args = Array.Empty() }, [1] = new Error() { Messages = new[] { "name21: {_name}", "path22: {_path}" }, Args = Array.Empty() }, [2] = DefaultErrors[2] }; var messageService = new MessageService(DefaultTranslations, errors, DefaultTemplate); var result = messageService.GetMessages( new Dictionary>() { ["some.not.registered.path"] = new List() { 0 }, ["another.path.#2.with.indexes.#12"] = new List() { 0, 1 }, }, "translation2"); result.Count.Should().Be(2); result.Keys.Should().Contain("some.not.registered.path"); result["some.not.registered.path"].Count.Should().Be(2); result["some.not.registered.path"].ElementAt(0).Should().Be("name11: path"); result["some.not.registered.path"].ElementAt(1).Should().Be("path12: some.not.registered.path"); result.Keys.Should().Contain("another.path.#2.with.indexes.#12"); result["another.path.#2.with.indexes.#12"].Count.Should().Be(4); result["another.path.#2.with.indexes.#12"].ElementAt(0).Should().Be("name11: #12"); result["another.path.#2.with.indexes.#12"].ElementAt(1).Should().Be("path12: another.path.#2.with.indexes.#12"); result["another.path.#2.with.indexes.#12"].ElementAt(2).Should().Be("name21: #12"); result["another.path.#2.with.indexes.#12"].ElementAt(3).Should().Be("path22: another.path.#2.with.indexes.#12"); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/ReferenceLoopErrorTests.cs ================================================ namespace Validot.Tests.Unit.Errors { using System; using System.Linq; using FluentAssertions; using Validot.Errors; using Validot.Translations; using Xunit; public class ReferenceLoopErrorTests { [Fact] public void Should_Initialize() { var error = new ReferenceLoopError(typeof(DateTimeOffset?)); error.Messages.Should().NotBeNull(); error.Messages.Count.Should().Be(1); error.Messages.Single().Should().Be(MessageKey.Global.ReferenceLoop); error.Codes.Should().NotBeNull(); error.Codes.Should().BeEmpty(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Errors/Translator/MessageTranslatorTests.cs ================================================ namespace Validot.Tests.Unit.Errors.Translator { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Errors; using Validot.Errors.Args; using Validot.Errors.Translator; using Xunit; public class MessageTranslatorTests { public class TranslateMessagesWithPathPlaceholders { private static readonly ArgPlaceholder ParameterlessNamePlaceholders = new ArgPlaceholder() { Name = "_name", Placeholder = "{_name}", Parameters = new Dictionary() }; private static readonly ArgPlaceholder TitleCaseNamePlaceholders = new ArgPlaceholder() { Name = "_name", Placeholder = "{_name|format=titleCase}", Parameters = new Dictionary() { ["format"] = "titleCase" } }; private static readonly ArgPlaceholder ParameterlessPathPlaceholders = new ArgPlaceholder() { Name = "_path", Placeholder = "{_path}", Parameters = new Dictionary() }; [Fact] public void Should_ThrowException_When_Path_IsNull() { var errorMessages = new[] { "message1", "message2 {_path}" }; var indexedPathsPlaceholders = new Dictionary>() { [1] = new[] { ParameterlessNamePlaceholders } }; Action action = () => MessageTranslator.TranslateMessagesWithPathPlaceholders(null, errorMessages, indexedPathsPlaceholders); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_ErrorMessages_IsNull() { var indexedPathsPlaceholders = new Dictionary>() { [1] = new[] { ParameterlessNamePlaceholders } }; Action action = () => MessageTranslator.TranslateMessagesWithPathPlaceholders("path", null, indexedPathsPlaceholders); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullIndexedPathHolders() { var errorMessages = new[] { "message1", "message2 {_path}" }; Action action = () => MessageTranslator.TranslateMessagesWithPathPlaceholders("path", errorMessages, null); action.Should().ThrowExactly(); } [Fact] public void Should_Translate_WithPathArgs() { var errorMessages = new[] { "message1", "message2 {_name} {_path}", "message3", "message4 {_name}", "message5 {_path}", }; var indexedPathsPlaceholders = new Dictionary>() { [1] = new[] { ParameterlessNamePlaceholders, ParameterlessPathPlaceholders }, [3] = new[] { ParameterlessNamePlaceholders }, [4] = new[] { ParameterlessPathPlaceholders } }; var results = MessageTranslator.TranslateMessagesWithPathPlaceholders("some.path", errorMessages, indexedPathsPlaceholders); results.Count.Should().Be(5); results[0].Should().Be("message1"); results[1].Should().Be("message2 path some.path"); results[2].Should().Be("message3"); results[3].Should().Be("message4 path"); results[4].Should().Be("message5 some.path"); } [Fact] public void Should_Translate_WithPathArgs_WithParameters() { var errorMessages = new[] { "message1", "message2 {_name|format=titleCase} {_path}", "message3 >{_name}<", "message4 '{_name|format=titleCase}'", "message5 {_path}", }; var indexedPathsPlaceholders = new Dictionary>() { [1] = new[] { TitleCaseNamePlaceholders, ParameterlessPathPlaceholders }, [2] = new[] { ParameterlessNamePlaceholders }, [3] = new[] { TitleCaseNamePlaceholders }, [4] = new[] { ParameterlessPathPlaceholders } }; var results = MessageTranslator.TranslateMessagesWithPathPlaceholders("some.path.veryImportantPath", errorMessages, indexedPathsPlaceholders); results.Count.Should().Be(5); results[0].Should().Be("message1"); results[1].Should().Be("message2 Very Important Path some.path.veryImportantPath"); results[2].Should().Be("message3 >veryImportantPath<"); results[3].Should().Be("message4 'Very Important Path'"); results[4].Should().Be("message5 some.path.veryImportantPath"); } public static IEnumerable Should_Translate_WithNameArg_Data() { yield return new object[] { "someWeirdName123", TitleCaseNamePlaceholders, "Message {_name|format=titleCase}", "Message Some Weird Name 123" }; yield return new object[] { "nested.path.someWeirdName123", TitleCaseNamePlaceholders, "Message {_name|format=titleCase}", "Message Some Weird Name 123" }; yield return new object[] { "very.nested.path.SetSlot123ToInput456", TitleCaseNamePlaceholders, "Message >{_name|format=titleCase}<", "Message >Set Slot 123 To Input 456<" }; yield return new object[] { "path.This_is_a_Test_of_Network123_in_12_days", TitleCaseNamePlaceholders, "XXX ### {_name|format=titleCase} ### XXX", "XXX ### This Is A Test Of Network 123 In 12 Days ### XXX" }; } [Theory] [MemberData(nameof(Should_Translate_WithNameArg_Data))] public void Should_Translate_WithNameArg(string path, ArgPlaceholder placeholder, string message, string expectedTranslatedMessage) { var indexedPathsPlaceholders = new Dictionary>() { [0] = new[] { placeholder } }; var results = MessageTranslator.TranslateMessagesWithPathPlaceholders(path, new[] { message }, indexedPathsPlaceholders); results.Count.Should().Be(1); results[0].Should().Be(expectedTranslatedMessage); } [Fact] public void Should_Translate_WithPathArgs_And_Leave_When_MissingPlaceholders() { var errorMessages = new[] { "message1", "message2 {_name} {_path}", "message3", "message4 {_name}", "message5 {_path}", }; var indexedPathsPlaceholders = new Dictionary>() { [1] = new[] { ParameterlessNamePlaceholders, }, [4] = new[] { ParameterlessPathPlaceholders } }; var results = MessageTranslator.TranslateMessagesWithPathPlaceholders("some.path", errorMessages, indexedPathsPlaceholders); results.Count.Should().Be(5); results[0].Should().Be("message1"); results[1].Should().Be("message2 path {_path}"); results[2].Should().Be("message3"); results[3].Should().Be("message4 {_name}"); results[4].Should().Be("message5 some.path"); } [Fact] public void Should_Translate_WithPathArgs_And_Leave_When_InvalidPlaceholders() { var errorMessages = new[] { "message1", "message2 {_name} {_path}", "message3", "message4 {_name}", "message5 {_path}", }; var indexedPathsPlaceholders = new Dictionary>() { [1] = new[] { ParameterlessNamePlaceholders, new ArgPlaceholder() { Name = "_invalid", Placeholder = "{_invalid}" } }, [2] = new[] { new ArgPlaceholder() { Name = "_invalid", Placeholder = "{_invalid}" } }, [4] = new[] { ParameterlessPathPlaceholders, new ArgPlaceholder() { Name = "_invalid", Placeholder = "{_invalid}" } } }; var results = MessageTranslator.TranslateMessagesWithPathPlaceholders("some.path", errorMessages, indexedPathsPlaceholders); results.Count.Should().Be(5); results[0].Should().Be("message1"); results[1].Should().Be("message2 path {_path}"); results[2].Should().Be("message3"); results[3].Should().Be("message4 {_name}"); results[4].Should().Be("message5 some.path"); } } public class TranslateMessages { [Fact] public void Should_ThrowException_When_NullTranslationName() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key2"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1" } }; Action action = () => translator.TranslateMessages(null, error); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullError() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key2"] = "message2" } }; var translator = new MessageTranslator(translations); Action action = () => translator.TranslateMessages("translation1", null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_TranslationNameNotFound() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key2"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1" }, Args = new IArg[] { } }; Action action = () => translator.TranslateMessages("translation3", error); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Error_ContainsNullMessage() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key2"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", null }, Args = new IArg[] { Arg.Text("test", "test") } }; Action action = () => translator.TranslateMessages("translation1", error); action.Should().ThrowExactly(); } [Fact] public void Should_Translate_UsingSelectedTranslation_When_KeyInTranslation() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key1"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1" }, Args = new IArg[] { } }; var result = translator.TranslateMessages("translation2", error); result.Messages.Count.Should().Be(1); result.Messages.Should().Contain("message2"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_ReturnSameMessage_When_KeyInTranslation() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key1"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key123" }, Args = new IArg[] { } }; var result = translator.TranslateMessages("translation2", error); result.Messages.Count.Should().Be(1); result.Messages.Should().Contain("key123"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_UsingTranslationForExistingKeys_And_ReturnSameMessageForKeyInTranslation() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1", ["key2"] = "message2", ["key3"] = "message3" }, ["translation2"] = new Dictionary { ["key1"] = "message11", ["key2"] = "message22", ["key3"] = "message33" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", "key2", "key123" }, Args = new IArg[] { } }; var result = translator.TranslateMessages("translation2", error); result.Messages.Count.Should().Be(3); result.Messages.Should().Contain("message11"); result.Messages.Should().Contain("message22"); result.Messages.Should().Contain("key123"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_And_UseArgs() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1 {arg1}", ["key2"] = "message2 {arg2}" }, ["translation2"] = new Dictionary { ["key1"] = "message11 {arg1}", ["key2"] = "message22 {arg1} {arg2}", } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", "key2", }, Args = new IArg[] { Arg.Text("arg1", "arg1Value"), Arg.Text("arg2", "arg2Value") } }; var result = translator.TranslateMessages("translation2", error); result.Messages.Count.Should().Be(2); result.Messages.Should().Contain("message11 arg1Value"); result.Messages.Should().Contain("message22 arg1Value arg2Value"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_And_UseArgs_WithParameters() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1 {arg1|case=upper} {arg1|case=lower}" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", }, Args = new IArg[] { Arg.Text("arg1", "arg1Value"), } }; var result = translator.TranslateMessages("translation1", error); result.Messages.Count.Should().Be(1); result.Messages.Should().Contain("message1 ARG1VALUE arg1value"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_And_UseArgs_Special_Translation() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1 {_translation|key=key2}", ["key2"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", }, Args = new IArg[] { } }; var result = translator.TranslateMessages("translation1", error); result.Messages.Count.Should().Be(1); result.Messages.Should().Contain("message1 message2"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_And_UseArgs_Special_Translation_Recursion() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1 {_translation|key=key1}", } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", }, Args = new IArg[] { } }; var result = translator.TranslateMessages("translation1", error); result.Messages.Count.Should().Be(1); result.Messages.Should().Contain("message1 message1 {_translation|key=key1}"); result.AnyPathPlaceholders.Should().Be(false); result.IndexedPathPlaceholders.Should().BeEmpty(); } [Fact] public void Should_Translate_And_ExtractPathPlaceholders() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1", } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", "message with path: {_path}", "message with name: {_name}", "message with path and name: {_path} {_name}", }, Args = new IArg[] { } }; var result = translator.TranslateMessages("translation1", error); result.Messages.Count.Should().Be(4); result.Messages.ElementAt(0).Should().Be("message1"); result.Messages.ElementAt(1).Should().Be("message with path: {_path}"); result.Messages.ElementAt(2).Should().Be("message with name: {_name}"); result.Messages.ElementAt(3).Should().Be("message with path and name: {_path} {_name}"); result.AnyPathPlaceholders.Should().Be(true); result.IndexedPathPlaceholders.Count.Should().Be(3); result.IndexedPathPlaceholders.Keys.Should().Contain(1); result.IndexedPathPlaceholders.Keys.Should().Contain(2); result.IndexedPathPlaceholders.Keys.Should().Contain(3); result.IndexedPathPlaceholders[1].Count.Should().Be(1); result.IndexedPathPlaceholders[1].ElementAt(0).Name.Should().Be("_path"); result.IndexedPathPlaceholders[1].ElementAt(0).Placeholder.Should().Be("{_path}"); result.IndexedPathPlaceholders[1].ElementAt(0).Parameters.Should().BeEmpty(); result.IndexedPathPlaceholders[2].Count.Should().Be(1); result.IndexedPathPlaceholders[2].ElementAt(0).Name.Should().Be("_name"); result.IndexedPathPlaceholders[2].ElementAt(0).Placeholder.Should().Be("{_name}"); result.IndexedPathPlaceholders[2].ElementAt(0).Parameters.Should().BeEmpty(); result.IndexedPathPlaceholders[3].Count.Should().Be(2); result.IndexedPathPlaceholders[3].ElementAt(0).Name.Should().Be("_path"); result.IndexedPathPlaceholders[3].ElementAt(0).Placeholder.Should().Be("{_path}"); result.IndexedPathPlaceholders[3].ElementAt(0).Parameters.Should().BeEmpty(); result.IndexedPathPlaceholders[3].ElementAt(1).Name.Should().Be("_name"); result.IndexedPathPlaceholders[3].ElementAt(1).Placeholder.Should().Be("{_name}"); result.IndexedPathPlaceholders[3].ElementAt(1).Parameters.Should().BeEmpty(); } [Fact] public void Should_ThrowException_When_Error_ContainsNullArg() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key2"] = "message2" } }; var translator = new MessageTranslator(translations); var error = new Error() { Messages = new[] { "key1", }, Args = new IArg[] { Arg.Text("test", "test"), null } }; Action action = () => translator.TranslateMessages("translation1", error); action.Should().ThrowExactly(); } } [Fact] public void Should_Initialize() { _ = new MessageTranslator(new Dictionary>()); } [Fact] public void Should_Set_TranslationNames() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1" }, ["translation2"] = new Dictionary { ["key2"] = "message2" }, ["translation3"] = new Dictionary { ["key3"] = "message3" }, ["translation4"] = new Dictionary { ["key4"] = "message4" } }; var translator = new MessageTranslator(translations); translator.TranslationNames.Should().NotBeNull(); translator.TranslationNames.Count.Should().Be(4); translator.TranslationNames.Should().Contain("translation1"); translator.TranslationNames.Should().Contain("translation2"); translator.TranslationNames.Should().Contain("translation3"); translator.TranslationNames.Should().Contain("translation4"); } [Fact] public void Should_Set_Translations() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1", ["key2"] = "message2" }, ["translation2"] = new Dictionary { ["key3"] = "message3", ["key4"] = "message4" } }; var translator = new MessageTranslator(translations); translator.Translations.Should().BeSameAs(translations); } [Fact] public void Should_Set_Translations_WithoutModification() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1", ["key2"] = "message2" }, ["translation2"] = new Dictionary { ["key3"] = "message3", ["key4"] = "message4" } }; var translator = new MessageTranslator(translations); translator.Translations.Keys.Should().Contain("translation1"); translator.Translations.Keys.Count().Should().Be(2); translator.Translations["translation1"].Keys.Count().Should().Be(2); translator.Translations["translation1"].Keys.Should().Contain("key1"); translator.Translations["translation1"]["key1"].Should().Be("message1"); translator.Translations["translation1"].Keys.Should().Contain("key2"); translator.Translations["translation1"]["key2"].Should().Be("message2"); translator.Translations["translation2"].Keys.Count().Should().Be(2); translator.Translations["translation2"].Keys.Should().Contain("key3"); translator.Translations["translation2"]["key3"].Should().Be("message3"); translator.Translations["translation2"].Keys.Should().Contain("key4"); translator.Translations["translation2"]["key4"].Should().Be("message4"); } [Fact] public void Should_Set_TranslationArgs() { var translations = new Dictionary> { ["translation1"] = new Dictionary { ["key11"] = "message11", ["key12"] = "message12" }, ["translation2"] = new Dictionary { ["key21"] = "message21" }, }; var translator = new MessageTranslator(translations); translator.TranslationArgs.Should().NotBeEmpty(); translator.TranslationArgs.Count.Should().Be(2); translator.TranslationArgs.Keys.Should().Contain("translation1"); translator.TranslationArgs["translation1"].Length.Should().Be(1); translator.TranslationArgs["translation1"][0].Should().BeAssignableTo(); translator.TranslationArgs["translation1"][0].Name.Should().Be("_translation"); translator.TranslationArgs["translation1"][0].ToString(new Dictionary() { ["key"] = "key11" }).Should().Be("message11"); translator.TranslationArgs["translation1"][0].ToString(new Dictionary() { ["key"] = "key12" }).Should().Be("message12"); translator.TranslationArgs["translation1"][0].ToString(new Dictionary() { ["key"] = "key21" }).Should().Be("key21"); translator.TranslationArgs["translation2"].Length.Should().Be(1); translator.TranslationArgs["translation2"][0].Should().BeAssignableTo(); translator.TranslationArgs["translation2"][0].Name.Should().Be("_translation"); translator.TranslationArgs["translation2"][0].ToString(new Dictionary() { ["key"] = "key11" }).Should().Be("key11"); translator.TranslationArgs["translation2"][0].ToString(new Dictionary() { ["key"] = "key12" }).Should().Be("key12"); translator.TranslationArgs["translation2"][0].ToString(new Dictionary() { ["key"] = "key21" }).Should().Be("message21"); } [Fact] public void Should_ThrowException_When_Initialize_With_NullEntryInTranslation() { Action action = () => new MessageTranslator(new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1", ["key2"] = "message2" }, ["translation2"] = new Dictionary { ["key1"] = "message1", ["key2"] = null } }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_NullTranslations() { Action action = () => new MessageTranslator(null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_SingleNullTranslations() { Action action = () => new MessageTranslator(new Dictionary> { ["translation1"] = new Dictionary { ["key1"] = "message1", ["key2"] = "message2" }, ["translation2"] = null }); action.Should().ThrowExactly(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Factory/HolderInfoTests.cs ================================================ namespace Validot.Tests.Unit.Factory { using System; using FluentAssertions; using Validot.Factory; using Validot.Settings; using Validot.Testing; using Xunit; public class HolderInfoTests { [Fact] public void Should_Initialize() { _ = new HolderInfo(typeof(ObjectSpecificationHolder), typeof(CustomClass)); } [Fact] public void Constructor_Should_ThrowException_When_HolderTypeIsNull() { Action action = () => _ = new HolderInfo(null, typeof(CustomClass)); action.Should().ThrowExactly().And.ParamName.Should().Be("holderType"); } [Fact] public void Constructor_Should_ThrowException_When_SpecifiedTypeIsNull() { Action action = () => _ = new HolderInfo(typeof(ObjectSpecificationHolder), null); action.Should().ThrowExactly().And.ParamName.Should().Be("specifiedType"); } [Fact] public void Should_AssignTypes() { var holderInfo = new HolderInfo(typeof(ObjectSpecificationHolder), typeof(CustomClass)); holderInfo.HolderType.Should().Be(typeof(ObjectSpecificationHolder)); holderInfo.SpecifiedType.Should().Be(typeof(CustomClass)); } [Fact] public void Should_ValidatorType_BeSetAs_IValidatorOfSpecifiedType() { var holderInfo = new HolderInfo(typeof(ObjectSpecificationHolder), typeof(CustomClass)); holderInfo.ValidatorType.Should().Be(typeof(IValidator)); } [Fact] public void Should_HoldsSettings_Should_BeFalse_When_HolderIsNotSettingsHolder() { var holderInfo = new HolderInfo(typeof(ObjectSpecificationHolder), typeof(CustomClass)); holderInfo.HoldsSettings.Should().BeFalse(); } [Fact] public void Should_HoldsSettings_Should_BeTrue_When_HolderIsSettingsHolder() { var holderInfo = new HolderInfo(typeof(ObjectSpecificationAndSettingsHolder), typeof(CustomClass)); holderInfo.HoldsSettings.Should().BeTrue(); } [Fact] public void Should_ThrowException_When_HolderTypeIsNotSpecificationHolderForSpecifiedType() { Action action = () => _ = new HolderInfo(typeof(ObjectSpecificationHolder), typeof(int)); var exception = action.Should().ThrowExactly().And; exception.ParamName.Should().Be("holderType"); exception.Message.Should().StartWith("ObjectSpecificationHolder is not a holder for Int32 specification (doesn't implement ISpecificationHolder)"); } [Fact] public void Should_ThrowException_When_HolderTypeDoesntHaveDefaultConstructor() { Action action = () => _ = new HolderInfo(typeof(HolderWithoutDefaultConstructor), typeof(CustomClass)); var exception = action.Should().ThrowExactly().And; exception.ParamName.Should().Be("holderType"); exception.Message.Should().StartWith("HolderWithoutDefaultConstructor must be a class and have parameterless constructor."); } [Fact] public void Should_ThrowException_When_HolderTypeIsNotClass() { Action action = () => _ = new HolderInfo(typeof(CustomStruct), typeof(CustomClass)); var exception = action.Should().ThrowExactly().And; exception.ParamName.Should().Be("holderType"); exception.Message.Should().StartWith("CustomStruct must be a class and have parameterless constructor."); } [Fact] public void Should_CreateValidator_ReturnValidatorInitializedWithSpecification() { var holderInfo = new HolderInfo(typeof(ObjectSpecificationHolder), typeof(CustomClass)); var createdValidator = holderInfo.CreateValidator(); createdValidator.Should().NotBeNull(); createdValidator.Should().BeOfType>(); var validator = (Validator)createdValidator; var case1 = new CustomClass() { CustomValue = "@b" }; var case2 = new CustomClass() { CustomValue = "abcdef" }; var case3 = new CustomClass() { CustomValue = "@bcdef" }; var case4 = new CustomClass() { CustomValue = "a" }; validator.Validate(case1).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "CustomValue: Min length is 3"); validator.Validate(case2).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "CustomValue: Must contain @"); validator.IsValid(case3).Should().BeTrue(); validator.Validate(case4).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "CustomValue: Min length is 3", "CustomValue: Must contain @"); } [Fact] public void Should_CreateValidator_ReturnValidatorInitializedWithSpecificationAndSettings() { var holderInfo = new HolderInfo(typeof(ObjectSpecificationAndSettingsHolder), typeof(CustomClass)); var createdValidator = holderInfo.CreateValidator(); createdValidator.Should().NotBeNull(); createdValidator.Should().BeOfType>(); var validator = (Validator)createdValidator; var case1 = new CustomClass() { CustomValue = "@b" }; var case2 = new CustomClass() { CustomValue = "abcdef" }; var case3 = new CustomClass() { CustomValue = "@bcdef" }; var case4 = new CustomClass() { CustomValue = "a" }; validator.Validate(case1).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "CustomValue: Must not contain @"); validator.Validate(case2).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "CustomValue: MUST HAVE MAX 3 CHARACTERS"); validator.Validate(case3).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "CustomValue: MUST HAVE MAX 3 CHARACTERS", "CustomValue: Must not contain @"); validator.IsValid(case4).Should().BeTrue(); } [Fact] public void Should_CreateValidator_ReturnValidatorInitializedWithSpecification_When_HolderHoldsMultipleSpecifications() { var intHolderInfo = new HolderInfo(typeof(MultipleSpecificationHolder), typeof(int)); var createdIntValidator = intHolderInfo.CreateValidator(); createdIntValidator.Should().NotBeNull(); createdIntValidator.Should().BeOfType>(); var intValidator = (Validator)createdIntValidator; intValidator.IsValid(5).Should().BeTrue(); intValidator.IsValid(9).Should().BeTrue(); intValidator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Value range 1-10 is allowed"); intValidator.Validate(11).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Value range 1-10 is allowed"); var stringHolderInfo = new HolderInfo(typeof(MultipleSpecificationHolder), typeof(string)); var createdStringValidator = stringHolderInfo.CreateValidator(); createdStringValidator.Should().NotBeNull(); createdStringValidator.Should().BeOfType>(); var stringValidator = (Validator)createdStringValidator; stringValidator.IsValid("abc").Should().BeTrue(); stringValidator.IsValid("abcdefgh").Should().BeTrue(); stringValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Length range 1-10 is allowed"); stringValidator.Validate("01234567890987654321").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Length range 1-10 is allowed"); } [Fact] public void Should_CreateValidator_ReturnValidatorInitializedWithSpecificationAndSettings_When_HolderHoldsMultipleSpecifications() { var intHolderInfo = new HolderInfo(typeof(MultipleSpecificationAndSettingsHolder), typeof(int)); var createdIntValidator = intHolderInfo.CreateValidator(); createdIntValidator.Should().NotBeNull(); createdIntValidator.Should().BeOfType>(); var intValidator = (Validator)createdIntValidator; intValidator.IsValid(5).Should().BeTrue(); intValidator.IsValid(9).Should().BeTrue(); intValidator.Validate(0).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Only values 1-10 are valid"); intValidator.Validate(11).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Only values 1-10 are valid"); var stringHolderInfo = new HolderInfo(typeof(MultipleSpecificationAndSettingsHolder), typeof(string)); var createdStringValidator = stringHolderInfo.CreateValidator(); createdStringValidator.Should().NotBeNull(); createdStringValidator.Should().BeOfType>(); var stringValidator = (Validator)createdStringValidator; stringValidator.IsValid("abc").Should().BeTrue(); stringValidator.IsValid("abcdefgh").Should().BeTrue(); stringValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Only values with length 1-10 are valid"); stringValidator.Validate("01234567890987654321").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Only values with length 1-10 are valid"); } internal class HolderWithoutDefaultConstructor : ISpecificationHolder { public HolderWithoutDefaultConstructor(string a) { _ = a; } public Specification Specification { get; set; } } internal class ObjectSpecificationHolder : ISpecificationHolder { public Specification Specification { get; } = s => s .Member(m => m.CustomValue, m => m .MinLength(3) .WithMessage("Min length is 3") .And() .Contains("@") .WithMessage("Must contain @")); } internal class ObjectSpecificationAndSettingsHolder : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; set; } = s => s .Member(m => m.CustomValue, m => m .MaxLength(3) .WithMessage("Max length is 3") .And() .NotContains("@") .WithMessage("Must not contain @")); public Func Settings { get; } = s => s .WithTranslation("English", "Max length is 3", "MUST HAVE MAX 3 CHARACTERS"); } internal class MultipleSpecificationHolder : ISpecificationHolder, ISpecificationHolder { Specification ISpecificationHolder.Specification { get; } = s => s.BetweenOrEqualTo(1, 10).WithMessage("Value range 1-10 is allowed"); Specification ISpecificationHolder.Specification { get; } = s => s.LengthBetween(1, 10).WithMessage("Length range 1-10 is allowed"); } internal class MultipleSpecificationAndSettingsHolder : ISpecificationHolder, ISpecificationHolder, ISettingsHolder { Specification ISpecificationHolder.Specification { get; } = s => s.BetweenOrEqualTo(1, 10).WithMessage("Value range 1-10 is allowed"); Specification ISpecificationHolder.Specification { get; } = s => s.LengthBetween(1, 10).WithMessage("Length range 1-10 is allowed"); public Func Settings { get; } = s => s .WithTranslation("English", "Value range 1-10 is allowed", "Only values 1-10 are valid") .WithTranslation("English", "Length range 1-10 is allowed", "Only values with length 1-10 are valid"); } internal class CustomClass { public string CustomValue { get; set; } } internal struct CustomStruct { public string CustomValue { get; set; } } } } ================================================ FILE: tests/Validot.Tests.Unit/Factory/ValidatorFactoryTests.cs ================================================ namespace Validot.Tests.Unit.Factory { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using AssemblyWithHolders; using FluentAssertions; using Validot.Factory; using Validot.Settings; using Validot.Testing; using Validot.Tests.Unit.Settings; using Validot.Tests.Unit.Translations; using Validot.Translations; using Xunit; public class ValidatorFactoryTests { public class ValidationWhenFromSpecification { [Theory] [MemberData(nameof(ValidationTestData.CasesForTemplate_Data), MemberType = typeof(ValidationTestData))] public void Should_HaveTemplate(string name, Specification specification, IReadOnlyDictionary> errorCases) { _ = name; var validator = Validator.Factory.Create(specification); validator.ShouldHaveTemplate(errorCases); } [Theory] [MemberData(nameof(ValidationTestData.CasesForValidation_Data), MemberType = typeof(ValidationTestData))] public void Should_Validate(string name, Specification specification, ValidationTestData.TestClass model, IReadOnlyDictionary> errorCases, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { _ = name; var validator = Validator.Factory.Create(specification); validator.ShouldValidateAndHaveResult(model, false, errorCases, exceptionCase); } [Theory] [MemberData(nameof(ValidationTestData.CasesForValidationWithFailFast_Data), MemberType = typeof(ValidationTestData))] public void Should_Validate_AndFailFast(string name, Specification specification, ValidationTestData.TestClass model, IReadOnlyDictionary> errorCases, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { _ = name; var validator = Validator.Factory.Create(specification); validator.ShouldValidateAndHaveResult(model, true, errorCases, exceptionCase); } [Theory] [MemberData(nameof(ValidationTestData.CasesForIsValid_Data), MemberType = typeof(ValidationTestData))] public void Should_IsValid_Return_True_If_NoErrors(string name, Specification specification, ValidationTestData.TestClass model, bool expectedIsValid, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { _ = name; var validator = Validator.Factory.Create(specification); validator.ShouldHaveIsValidTrueIfNoErrors(model, expectedIsValid, exceptionCase); } } public class ValidationWhenFromHolder { [Theory] [MemberData(nameof(ValidationTestData.CasesForTemplate_Data), MemberType = typeof(ValidationTestData))] public void Should_HaveTemplate(string name, Specification specification, IReadOnlyDictionary> errorCases) { _ = name; var holder = new TestClassSpecificationHolder() { Specification = specification }; var validator = Validator.Factory.Create(holder); validator.ShouldHaveTemplate(errorCases); } [Theory] [MemberData(nameof(ValidationTestData.CasesForValidation_Data), MemberType = typeof(ValidationTestData))] public void Should_Validate(string name, Specification specification, ValidationTestData.TestClass model, IReadOnlyDictionary> errorCases, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { _ = name; var holder = new TestClassSpecificationHolder() { Specification = specification }; var validator = Validator.Factory.Create(holder); validator.ShouldValidateAndHaveResult(model, false, errorCases, exceptionCase); } [Theory] [MemberData(nameof(ValidationTestData.CasesForValidationWithFailFast_Data), MemberType = typeof(ValidationTestData))] public void Should_Validate_AndFailFast(string name, Specification specification, ValidationTestData.TestClass model, IReadOnlyDictionary> errorCases, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { _ = name; var holder = new TestClassSpecificationHolder() { Specification = specification }; var validator = Validator.Factory.Create(holder); validator.ShouldValidateAndHaveResult(model, true, errorCases, exceptionCase); } [Theory] [MemberData(nameof(ValidationTestData.CasesForIsValid_Data), MemberType = typeof(ValidationTestData))] public void Should_IsValid_Return_True_If_NoErrors(string name, Specification specification, ValidationTestData.TestClass model, bool expectedIsValid, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { _ = name; var holder = new TestClassSpecificationHolder() { Specification = specification }; var validator = Validator.Factory.Create(holder); validator.ShouldHaveIsValidTrueIfNoErrors(model, expectedIsValid, exceptionCase); } } public class SettingsFromInlineBuilder { [Fact] public void Should_LockSettings() { var validator = Validator.Factory.Create(s => s, s => s); ((ValidatorSettings)validator.Settings).IsLocked.Should().BeTrue(); } [Fact] public void Should_SetSettings() { var validator = Validator.Factory.Create( s => s, s => s .WithTranslation("a", "a", "a") .WithTranslation("a", "b", "c") .WithTranslation("x", "y", "z") .WithReferenceLoopProtectionDisabled() ); validator.Settings.Should().NotBeNull(); validator.Settings.Translations.Should().NotBeNull(); validator.Settings.Translations.ShouldBeLikeTranslations(new Dictionary>() { ["English"] = Translation.English, ["a"] = new Dictionary() { ["a"] = "a", ["b"] = "c" }, ["x"] = new Dictionary() { ["y"] = "z" } }); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeFalse(); } [Fact] public void Should_SetSettings_WhenSpecificationIsFromHolder() { var holder = new TestClassSpecificationHolder() { Specification = s => s }; var validator = Validator.Factory.Create( holder, s => s .WithTranslation("a", "a", "a") .WithTranslation("a", "b", "c") .WithTranslation("x", "y", "z") .WithReferenceLoopProtection() ); validator.Settings.Should().NotBeNull(); validator.Settings.Translations.Should().NotBeNull(); validator.Settings.Translations.ShouldBeLikeTranslations(new Dictionary>() { ["English"] = Translation.English, ["a"] = new Dictionary() { ["a"] = "a", ["b"] = "c" }, ["x"] = new Dictionary() { ["y"] = "z" } }); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); } [Fact] public void Should_ThrowException_When_PassingExternalSettings() { Action action = () => _ = Validator.Factory.Create(s => s, s => new ValidatorSettings()); var exception = action.Should().ThrowExactly().And; exception.Message.Should().Be("Validator settings fluent API should return the same reference as received."); } } public class SettingsFromHolder { [Fact] public void Should_LockSettings() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = s => s, Settings = s => s }; var validator = Validator.Factory.Create(holder); ((ValidatorSettings)validator.Settings).IsLocked.Should().BeTrue(); } [Fact] public void Should_LockSettings_When_OverridenByInlineBuilder() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = s => s, Settings = s => s }; var validator = Validator.Factory.Create(holder, s => s); ((ValidatorSettings)validator.Settings).IsLocked.Should().BeTrue(); } [Fact] public void Should_SetSettings() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = s => s, Settings = s => s .WithTranslation("a", "a", "a") .WithTranslation("a", "b", "c") .WithTranslation("x", "y", "z") .WithReferenceLoopProtection() }; var validator = Validator.Factory.Create(holder); validator.Settings.Should().NotBeNull(); validator.Settings.Translations.Should().NotBeNull(); validator.Settings.Translations.ShouldBeLikeTranslations(new Dictionary>() { ["English"] = Translation.English, ["a"] = new Dictionary() { ["a"] = "a", ["b"] = "c" }, ["x"] = new Dictionary() { ["y"] = "z" } }); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); } [Fact] public void Should_InlineSettings_Overwrite_SettingsFromHolder() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = s => s, Settings = s => s .WithTranslation("a", "a", "AAA") .WithTranslation("x", "y", "ZZZ") .WithReferenceLoopProtectionDisabled() }; var validator = Validator.Factory.Create(holder, s => s .WithTranslation("a", "a", "a") .WithTranslation("a", "b", "c") .WithTranslation("x", "y", "z") .WithReferenceLoopProtection() ); validator.Settings.Should().NotBeNull(); validator.Settings.Translations.Should().NotBeNull(); validator.Settings.Translations.ShouldBeLikeTranslations(new Dictionary>() { ["English"] = Translation.English, ["a"] = new Dictionary() { ["a"] = "a", ["b"] = "c" }, ["x"] = new Dictionary() { ["y"] = "z" } }); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); } [Fact] public void Should_ThrowException_When_PassingExternalSettings_UsingHolder() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = s => s, Settings = s => new ValidatorSettings() }; Action action = () => _ = Validator.Factory.Create(holder); var exception = action.Should().ThrowExactly().And; exception.Message.Should().Be("Validator settings fluent API should return the same reference as received."); } [Fact] public void Should_ThrowException_When_PassingExternalSettings_UsingHolder_AndInlineBuilder() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = s => s, Settings = s => s }; Action action = () => _ = Validator.Factory.Create(holder, s => new ValidatorSettings()); var exception = action.Should().ThrowExactly().And; exception.Message.Should().Be("Validator settings fluent API should return the same reference as received."); } [Fact] public void Should_ThrowException_When_SpecificationHolder_IsNull() { Action action = () => _ = Validator.Factory.Create(null as ISpecificationHolder); action.Should().Throw().And.ParamName.Should().Be("specificationHolder"); } [Fact] public void Should_ThrowException_When_SpecificationHolder_ContainsNullSpecification() { var holder = new TestClassSpecificationAndSettingsHolder() { Specification = null }; Action action = () => _ = Validator.Factory.Create(holder); var exception = action.Should().Throw().And; exception.ParamName.Should().Be("specificationHolder"); exception.Message.Should().StartWith("ISettingsHolder can't have null Settings"); } } public class SettingsFromObject { [Fact] public void Should_LockSettings() { var validatorWithSettings = Validator.Factory.Create( s => s, s => s ); var validator = Validator.Factory.Create( s => s, validatorWithSettings.Settings ); ((ValidatorSettings)validator.Settings).IsLocked.Should().BeTrue(); } [Fact] public void Should_SetSettings() { var validatorWithSettings = Validator.Factory.Create( s => s, s => s .WithTranslation("a", "a", "a") .WithTranslation("a", "b", "c") .WithTranslation("x", "y", "z") .WithReferenceLoopProtectionDisabled() ); var validator = Validator.Factory.Create( s => s, validatorWithSettings.Settings ); validator.Settings.Should().NotBeNull(); validator.Settings.Should().BeSameAs(validatorWithSettings.Settings); validator.Settings.Translations.Should().NotBeNull(); validator.Settings.Translations.ShouldBeLikeTranslations(new Dictionary>() { ["English"] = Translation.English, ["a"] = new Dictionary() { ["a"] = "a", ["b"] = "c" }, ["x"] = new Dictionary() { ["y"] = "z" } }); validator.Settings.ReferenceLoopProtectionEnabled.Should().BeFalse(); } [Fact] public void Should_ThrowException_When_NullSettings() { Action action = () => _ = Validator.Factory.Create(s => s, null as IValidatorSettings); action.Should().ThrowExactly().And.ParamName.Should().Be("settings"); } [Fact] public void Should_ThrowException_When_CustomSettingsInterfaceImplementation() { Action action = () => _ = Validator.Factory.Create(s => s, new CustomSettings()); var exception = action.Should().ThrowExactly().And; exception.ParamName.Should().Be("settings"); exception.Message.Should().StartWith("Custom IValidatorSettings implementations are not supported."); } internal class CustomSettings : IValidatorSettings { public IReadOnlyDictionary> Translations { get; set; } public bool ReferenceLoopProtectionEnabled { get; set; } } } public class Translating { [Fact] public void Should_WithTranslation_AddNewTranslations_AddingNamesWithDictionaries() { var dictionary1 = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "WILL_BE_OVERWRITTEN", ["k14"] = "WILL_BE_OVERWRITTEN", }; var dictionary2 = new Dictionary() { ["k21"] = "WILL_BE_OVERWRITTEN", ["k22"] = "WILL_BE_OVERWRITTEN", ["k23"] = "v23", ["k24"] = "v24", }; var dictionary3 = new Dictionary() { ["k13"] = "v13", ["k14"] = "v14", }; var dictionary4 = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", }; var validator = Validator.Factory.Create(s => s, o => o .WithTranslation("name1", dictionary1) .WithTranslation("name2", dictionary2) .WithTranslation("name1", dictionary3) .WithTranslation("name2", dictionary4)); validator.Settings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["English"] = Translation.English, ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", }, }); } [Fact] public void Should_WithTranslation_AddNewTranslations_AddingFullDictionary() { var dictionary1 = new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "WILL_BE_OVERWRITTEN", ["k12"] = "v12", }, ["name2"] = new Dictionary() { ["k23"] = "v23", ["k24"] = "v24", } }; var dictionary2 = new Dictionary>() { ["name1"] = new Dictionary() { ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "WILL_BE_OVERWRITTEN", } }; var dictionary3 = new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", }, ["name2"] = new Dictionary() { ["k22"] = "v22", } }; var validator = Validator.Factory.Create(s => s, o => o .WithTranslation(dictionary1) .WithTranslation(dictionary2) .WithTranslation(dictionary3)); validator.Settings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["English"] = Translation.English, ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", }, }); } [Fact] public void Should_WithTranslation_ChangeDefaultTranslation() { var dictionary1 = new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", }, ["English"] = new Dictionary() { ["Global.Required"] = "OVERWRITTEN_REQUIRED", ["TimeSpanType.GreaterThanOrEqualTo"] = "OVERWRITTEN_1" } }; var dictionary2 = new Dictionary() { ["Global.Required"] = "DOUBLE_OVERWRITTEN_REQUIRED", ["Tests.Email"] = "OVERWRITTEN_2", }; var dictionary3 = new Dictionary>() { ["name"] = new Dictionary() { ["k2"] = "v2", }, ["English"] = new Dictionary() { ["TotallyNewKey"] = "NEW_KEY", ["Tests.Email"] = "DOUBLE_OVERWRITTEN_2", } }; var validator = Validator.Factory.Create(s => s, o => o .WithTranslation(dictionary1) .WithTranslation("English", dictionary2) .WithTranslation(dictionary3)); var expectedModifiedEnglish = new Dictionary(); foreach (var pair in Translation.English) { expectedModifiedEnglish.Add(pair.Key, pair.Value); } expectedModifiedEnglish.Add("TotallyNewKey", "NEW_KEY"); expectedModifiedEnglish["Tests.Email"] = "DOUBLE_OVERWRITTEN_2"; expectedModifiedEnglish["Global.Required"] = "DOUBLE_OVERWRITTEN_REQUIRED"; expectedModifiedEnglish["TimeSpanType.GreaterThanOrEqualTo"] = "OVERWRITTEN_1"; validator.Settings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["English"] = expectedModifiedEnglish, ["name"] = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", }, }); } [Fact] public void Should_AddTranslationFromSettingsHolder() { var holder = new TestClassSpecificationAndTranslationHolder() { Specification = c => c, Settings = s => s .WithTranslation(new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", }, ["English"] = new Dictionary() { ["Global.Required"] = "OVERWRITTEN", ["TotallyNewKey"] = "NEW", } }) }; var validator = Validator.Factory.Create(holder); var expectedModifiedEnglish = new Dictionary(); foreach (var pair in Translation.English) { expectedModifiedEnglish.Add(pair.Key, pair.Value); } expectedModifiedEnglish.Add("TotallyNewKey", "NEW"); expectedModifiedEnglish["Global.Required"] = "OVERWRITTEN"; validator.Settings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["English"] = expectedModifiedEnglish, ["name"] = new Dictionary() { ["k1"] = "v1", }, }); } [Fact] public void Should_AddTranslationFromSettingsHolder_AndModifyItByWithTranslationFromSettings() { var holder = new TestClassSpecificationAndTranslationHolder() { Specification = c => c, Settings = s => s .WithTranslation(new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", }, ["English"] = new Dictionary() { ["Global.Required"] = "OVERWRITTEN", ["TotallyNewKey"] = "NEW", } }) }; var dictionary = new Dictionary>() { ["name"] = new Dictionary() { ["k2"] = "v2", }, ["English"] = new Dictionary() { ["Global.Required"] = "DOUBLE_OVERWRITTEN", ["TimeSpanType.GreaterThanOrEqualTo"] = "ANOTHER_OVERWRITTEN", ["TotallyNewKey2"] = "NEW2", } }; var validator = Validator.Factory.Create(holder, o => o.WithTranslation(dictionary)); var expectedModifiedEnglish = new Dictionary(); foreach (var pair in Translation.English) { expectedModifiedEnglish.Add(pair.Key, pair.Value); } expectedModifiedEnglish.Add("TotallyNewKey", "NEW"); expectedModifiedEnglish.Add("TotallyNewKey2", "NEW2"); expectedModifiedEnglish["Global.Required"] = "DOUBLE_OVERWRITTEN"; expectedModifiedEnglish["TimeSpanType.GreaterThanOrEqualTo"] = "ANOTHER_OVERWRITTEN"; validator.Settings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["English"] = expectedModifiedEnglish, ["name"] = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", }, }); } public class TestClassSpecificationAndTranslationHolder : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; set; } public Func Settings { get; set; } } } public class FetchHolders { [Fact] public void Should_FetchAllHoldersFromAssembly() { var holders = Validator.Factory.FetchHolders(typeof(AssemblyWithHoldersHook).Assembly); holders.Should().HaveCount(13); holders.Should().Contain(h => h.HolderType == typeof(HolderOfDecimalSpecification) && h.SpecifiedType == typeof(decimal) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfIntSpecificationAndSettings) && h.SpecifiedType == typeof(int) && h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfStringSpecification) && h.SpecifiedType == typeof(string) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfStringSpecificationAndSettings) && h.SpecifiedType == typeof(string) && h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfMultipleSpecifications) && h.SpecifiedType == typeof(DateTime) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfMultipleSpecifications) && h.SpecifiedType == typeof(DateTimeOffset) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfMultipleSpecificationsAndSettings) && h.SpecifiedType == typeof(float) && h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(HolderOfMultipleSpecificationsAndSettings) && h.SpecifiedType == typeof(double) && h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(NestedHolders.NestedHolderOfBoolSpecification) && h.SpecifiedType == typeof(bool) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(NestedHolders.NestedHolderOfStringSpecification) && h.SpecifiedType == typeof(string) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(NestedHolders.NestedHolderOfStringSpecificationAndSettings) && h.SpecifiedType == typeof(string) && h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(PrivateSpecificationHolder) && h.SpecifiedType == typeof(string) && !h.HoldsSettings); holders.Should().Contain(h => h.HolderType == typeof(PrivateSpecificationAndSettingsHolder) && h.SpecifiedType == typeof(string) && h.HoldsSettings); } [Fact] public void Should_FetchAllHoldersFromMultipleAssemblies() { var thisTestsHolders = Validator.Factory.FetchHolders(typeof(ValidatorFactoryTests).Assembly); var separateAssemblyHolders = Validator.Factory.FetchHolders(typeof(AssemblyWithHoldersHook).Assembly); var holders = Validator.Factory.FetchHolders(typeof(AssemblyWithHoldersHook).Assembly, typeof(ValidatorFactoryTests).Assembly); holders.Should().HaveCount(separateAssemblyHolders.Count + thisTestsHolders.Count); foreach (var holder in separateAssemblyHolders) { holders.Should().Contain(h => h.HolderType == holder.HolderType && h.SpecifiedType == holder.SpecifiedType && h.HoldsSettings == holder.HoldsSettings && h.ValidatorType == holder.ValidatorType); } foreach (var holder in thisTestsHolders) { holders.Should().Contain(h => h.HolderType == holder.HolderType && h.SpecifiedType == holder.SpecifiedType && h.HoldsSettings == holder.HoldsSettings && h.ValidatorType == holder.ValidatorType); } } [Fact] public void Should_FetchAllHolders_And_CreateValidatorsOutOfThem() { var holders = Validator.Factory.FetchHolders(typeof(AssemblyWithHoldersHook).Assembly); var holderOfDecimalSpecificationValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfDecimalSpecification) && h.SpecifiedType == typeof(decimal) && !h.HoldsSettings).CreateValidator(); holderOfDecimalSpecificationValidator.Validate(10.01M).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Max value is 10"); var holderOfIntSpecificationAndSettingsValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfIntSpecificationAndSettings) && h.SpecifiedType == typeof(int) && h.HoldsSettings).CreateValidator(); holderOfIntSpecificationAndSettingsValidator.Validate(11).ToString("BinaryEnglish").ShouldResultToStringHaveLines( ToStringContentType.Messages, "The maximum value is 0b1010"); var holderOfStringSpecificationValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfStringSpecification) && h.SpecifiedType == typeof(string) && !h.HoldsSettings).CreateValidator(); holderOfStringSpecificationValidator.Validate("!").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Text shorter than 3 characters not allowed", "Text containing exclamation mark not allowed"); var holderOfStringSpecificationAndSettingsValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfStringSpecificationAndSettings) && h.SpecifiedType == typeof(string) && h.HoldsSettings).CreateValidator(); holderOfStringSpecificationAndSettingsValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Empty string is invalid!", "Only strings of length from 3 to 10 are allowed" ); var holderOfMultipleSpecificationsDateTimeValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfMultipleSpecifications) && h.SpecifiedType == typeof(DateTime) && !h.HoldsSettings).CreateValidator(); holderOfMultipleSpecificationsDateTimeValidator.Validate(new DateTime(1999, 10, 10)).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Dates after 1st of Jan'00 are allowed" ); var holderOfMultipleSpecificationsDateTimeOffsetValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfMultipleSpecifications) && h.SpecifiedType == typeof(DateTimeOffset) && !h.HoldsSettings).CreateValidator(); holderOfMultipleSpecificationsDateTimeOffsetValidator.Validate(new DateTimeOffset(2077, 10, 10, 10, 10, 10, TimeSpan.Zero)).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Dates before midnight 1st of Jan'21 are allowed"); var holderOfMultipleSpecificationsAndSettingsFloatValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfMultipleSpecificationsAndSettings) && h.SpecifiedType == typeof(float) && h.HoldsSettings).CreateValidator(); holderOfMultipleSpecificationsAndSettingsFloatValidator.Validate(0.99F).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Minimum value is 1"); var holderOfMultipleSpecificationsAndSettingsDoubleValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfMultipleSpecificationsAndSettings) && h.SpecifiedType == typeof(double) && h.HoldsSettings).CreateValidator(); holderOfMultipleSpecificationsAndSettingsDoubleValidator.Validate(10.001D).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Maximum value is 10" ); var nestedHolderOfBoolSpecificationValidator = (Validator)holders.Single(h => h.HolderType == typeof(NestedHolders.NestedHolderOfBoolSpecification) && h.SpecifiedType == typeof(bool) && !h.HoldsSettings).CreateValidator(); nestedHolderOfBoolSpecificationValidator.Validate(false).ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must be true"); var nestedHolderOfStringSpecificationValidator = (Validator)holders.Single(h => h.HolderType == typeof(NestedHolders.NestedHolderOfStringSpecification) && h.SpecifiedType == typeof(string) && !h.HoldsSettings).CreateValidator(); nestedHolderOfStringSpecificationValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be empty"); var nestedHolderOfStringSpecificationAndSettingsValidator = (Validator)holders.Single(h => h.HolderType == typeof(NestedHolders.NestedHolderOfStringSpecificationAndSettings) && h.SpecifiedType == typeof(string) && h.HoldsSettings).CreateValidator(); nestedHolderOfStringSpecificationAndSettingsValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Cannot be empty!"); var privateSpecificationHolderValidator = (Validator)holders.Single(h => h.HolderType == typeof(PrivateSpecificationHolder) && h.SpecifiedType == typeof(string) && !h.HoldsSettings).CreateValidator(); privateSpecificationHolderValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be empty" ); var privateSpecificationAndSettingsHolderValidator = (Validator)holders.Single(h => h.HolderType == typeof(PrivateSpecificationAndSettingsHolder) && h.SpecifiedType == typeof(string) && h.HoldsSettings).CreateValidator(); privateSpecificationAndSettingsHolderValidator.Validate("").ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "Must not be empty" ); privateSpecificationAndSettingsHolderValidator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); } [Fact] public void Should_FetchHolders_And_InitializeValidatorsWithSettings() { var holders = Validator.Factory.FetchHolders(typeof(AssemblyWithHoldersHook).Assembly); var holderOfIntSpecificationAndSettingsValidator = (Validator)holders.Single(h => h.HolderType == typeof(HolderOfIntSpecificationAndSettings) && h.SpecifiedType == typeof(int) && h.HoldsSettings).CreateValidator(); holderOfIntSpecificationAndSettingsValidator.Settings.ReferenceLoopProtectionEnabled.Should().BeTrue(); holderOfIntSpecificationAndSettingsValidator.Settings.Translations.Keys.Should().Contain("English"); holderOfIntSpecificationAndSettingsValidator.Settings.Translations["English"]["Min value is 1"].Should().Be("The minimum value is 1"); holderOfIntSpecificationAndSettingsValidator.Settings.Translations["English"]["Max value is 10"].Should().Be("The maximum value is 10"); holderOfIntSpecificationAndSettingsValidator.Settings.Translations.Keys.Should().Contain("BinaryEnglish"); holderOfIntSpecificationAndSettingsValidator.Settings.Translations["BinaryEnglish"].Should().HaveCount(2); holderOfIntSpecificationAndSettingsValidator.Settings.Translations["BinaryEnglish"].Keys.Should().HaveCount(2); holderOfIntSpecificationAndSettingsValidator.Settings.Translations["BinaryEnglish"]["Min value is 1"].Should().Be("The minimum value is 0b0001"); holderOfIntSpecificationAndSettingsValidator.Settings.Translations["BinaryEnglish"]["Max value is 10"].Should().Be("The maximum value is 0b1010"); } [Fact] public void Should_ThrowException_When_Assemblies_IsEmpty() { Action action = () => Validator.Factory.FetchHolders(Array.Empty()); var exception = action.Should().ThrowExactly().And; exception.ParamName.Should().Be("assemblies"); exception.Message.Should().StartWith("Assembly collection must not be empty"); } [Fact] public void Should_ThrowException_When_Assemblies_ContainsNull() { Action action = () => Validator.Factory.FetchHolders(typeof(AssemblyWithHoldersHook).Assembly, null); action.Should().ThrowExactly(); } } [Fact] public void Should_HaveDefaultSettings() { var validator = Validator.Factory.Create(s => s); validator.Settings.ShouldBeLikeDefault(); } [Fact] public void Should_HaveDefaultSettingsLocked() { var validator = Validator.Factory.Create(s => s); ((ValidatorSettings)validator.Settings).IsLocked.Should().BeTrue(); } public class TestClassSpecificationAndSettingsHolder : ISpecificationHolder, ISettingsHolder { public Specification Specification { get; set; } public Func Settings { get; set; } } public class TestClassSpecificationHolder : ISpecificationHolder { public Specification Specification { get; set; } } } } ================================================ FILE: tests/Validot.Tests.Unit/GuardTests.cs ================================================ namespace Validot.Tests.Unit { using System; using FluentAssertions; using Xunit; public class GuardTests { public class NullArgument { [Fact] public void Should_Throw_When_ArgumentIsNull() { Action action = () => { ThrowHelper.NullArgument(null, "some name"); }; action.Should() .ThrowExactly() .WithMessage("*some name*"); } } } } ================================================ FILE: tests/Validot.Tests.Unit/PathHelperTests.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using FluentAssertions; using Xunit; public class PathHelperTests { [Fact] public void Should_Initialize() { PathHelper.Divider.Should().Be('.'); PathHelper.UpperLevelPointer.Should().Be('<'); PathHelper.CollectionIndexPrefix.Should().Be('#'); } public class ResolvePath { [Fact] public void Should_ThrowException_When_NullBasePath() { Action action = () => PathHelper.ResolvePath(null, "new"); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullRelativePath() { Action action = () => PathHelper.ResolvePath("base", null); action.Should().ThrowExactly(); } [Fact] public void Should_ReturnBasePath_When_RelativePathIsEmpty() { var result = PathHelper.ResolvePath("base.path", string.Empty); result.Should().Be("base.path"); } [Fact] public void Should_ReturnNewSegment_When_BasePathIsEmpty() { var result = PathHelper.ResolvePath(string.Empty, "new.segment"); result.Should().Be("new.segment"); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.SimpleRelativePath), MemberType = typeof(PathTestData.ResolvePath))] public void Should_Resolve_When_RelativePathIsSimple(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.RelativePathContainsMoreLevelsDown), MemberType = typeof(PathTestData.ResolvePath))] public void Should_Resolve_When_RelativePathContainsMoreLevelsDown(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.UncommonCharacters), MemberType = typeof(PathTestData.ResolvePath))] public void Should_Resolve_When_UncommonCharacters(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.RelativePathGoesLevelUp), MemberType = typeof(PathTestData.ResolvePath))] public void Should_Resolve_When_RelativePathGoesLevelUp(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.RelativePathIsEmpty_And_GoesLevelUp), MemberType = typeof(PathTestData.ResolvePath))] public void Should_Resolve_When_RelativePathIsEmpty_And_GoesLevelUp(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.RelativePathGoesLevelUp_And_ExceedsMinimumLevel), MemberType = typeof(PathTestData.ResolvePath))] public void Should_ReturnNewSegment_When_RelativePathGoesLevelUp_And_ExceedsMinimumLevel(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.ResolvePath.ToSamePath), MemberType = typeof(PathTestData.ResolvePath))] public void Should_ReturnNewSegment_When_ToSamePath(string basePath, string relativePath, string expectedPath) { var result = PathHelper.ResolvePath(basePath, relativePath); result.Should().Be(expectedPath); } } public class GetWithIndexes { [Fact] public void Should_ThrowException_When_NullIndexes() { Action action = () => PathHelper.GetWithIndexes("path", null); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(PathTestData.GetWithIndexes.LargeIndexes), MemberType = typeof(PathTestData.GetWithIndexes))] [MemberData(nameof(PathTestData.GetWithIndexes.CommonCases), MemberType = typeof(PathTestData.GetWithIndexes))] [MemberData(nameof(PathTestData.GetWithIndexes.TrickyCases), MemberType = typeof(PathTestData.GetWithIndexes))] [MemberData(nameof(PathTestData.GetWithIndexes.LargeIndexes), MemberType = typeof(PathTestData.GetWithIndexes))] public void Should_ReturnSamePath_When_NoIndexes(string path, IReadOnlyCollection indexesStack, string expectedPath) { _ = indexesStack; _ = expectedPath; var resolvedPath = PathHelper.GetWithIndexes(path, Array.Empty()); resolvedPath.Should().Be(path); } [Theory] [MemberData(nameof(PathTestData.GetWithIndexes.CommonCases), MemberType = typeof(PathTestData.GetWithIndexes))] public void Should_Resolve_CommonCases(string path, IReadOnlyCollection indexesStack, string expectedPath) { var resolvedPath = PathHelper.GetWithIndexes(path, indexesStack); resolvedPath.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.GetWithIndexes.TrickyCases), MemberType = typeof(PathTestData.GetWithIndexes))] public void Should_Resolve_TrickyCases(string path, IReadOnlyCollection indexesStack, string expectedPath) { var resolvedPath = PathHelper.GetWithIndexes(path, indexesStack); resolvedPath.Should().Be(expectedPath); } [Theory] [MemberData(nameof(PathTestData.GetWithIndexes.LargeIndexes), MemberType = typeof(PathTestData.GetWithIndexes))] public void Should_Resolve_LargeIndexes(string path, IReadOnlyCollection indexesStack, string expectedPath) { var resolvedPath = PathHelper.GetWithIndexes(path, indexesStack); resolvedPath.Should().Be(expectedPath); } } [Theory] [MemberData(nameof(PathTestData.GetWithIndexes.LargeIndexes), MemberType = typeof(PathTestData.GetWithIndexes))] [MemberData(nameof(PathTestData.GetWithIndexes.CommonCases), MemberType = typeof(PathTestData.GetWithIndexes))] [MemberData(nameof(PathTestData.GetWithIndexes.TrickyCases), MemberType = typeof(PathTestData.GetWithIndexes))] [MemberData(nameof(PathTestData.GetWithIndexes.LargeIndexes), MemberType = typeof(PathTestData.GetWithIndexes))] public void GetWithoutIndexes(string path, IReadOnlyCollection indexesStack, string pathWithIndexes) { _ = indexesStack; var pathWithoutIndexes = PathHelper.GetWithoutIndexes(pathWithIndexes); pathWithoutIndexes.Should().Be(path); } public class ContainsIndexes { [Fact] public void Should_ThrowException_When_NullPath() { Action action = () => PathHelper.ContainsIndexes(null); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.ResolvedIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnTrue_When_ResolvedIndexes(string path, int amount) { _ = amount; var containsIndexes = PathHelper.ContainsIndexes(path); containsIndexes.Should().Be(true); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.PlaceholdersIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnTrue_When_PlaceholdersIndexes(string path, int amount) { _ = amount; var containsIndexes = PathHelper.ContainsIndexes(path); containsIndexes.Should().Be(true); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.WeirdIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnTrue_When_WeirdIndexes(string path, int amount) { _ = amount; var containsIndexes = PathHelper.ContainsIndexes(path); containsIndexes.Should().Be(true); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.InvalidIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnFalse_When_InvalidIndexes(string path) { var containsIndexes = PathHelper.ContainsIndexes(path); containsIndexes.Should().Be(false); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.NoIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnFalse_When_NoIndexes(string path) { var containsIndexes = PathHelper.ContainsIndexes(path); containsIndexes.Should().Be(false); } } public class GetIndexesAmount { [Fact] public void Should_ThrowException_When_NullPath() { Action action = () => PathHelper.GetIndexesAmount(null); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.ResolvedIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_GetAmount_When_ResolvedIndexes(string path, int expectedAmount) { var amount = PathHelper.GetIndexesAmount(path); amount.Should().Be(expectedAmount); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.PlaceholdersIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_GetAmount_When_PlaceholdersIndexes(string path, int expectedAmount) { var amount = PathHelper.GetIndexesAmount(path); amount.Should().Be(expectedAmount); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.WeirdIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_GetAmount_When_WeirdIndexes(string path, int expectedAmount) { var amount = PathHelper.GetIndexesAmount(path); amount.Should().Be(expectedAmount); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.InvalidIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnZero_When_InvalidIndexes(string path) { var amount = PathHelper.GetIndexesAmount(path); amount.Should().Be(0); } [Theory] [MemberData(nameof(PathTestData.GetIndexesAmount.NoIndexes), MemberType = typeof(PathTestData.GetIndexesAmount))] public void Should_ReturnZero_When_NoIndexes(string path) { var amount = PathHelper.GetIndexesAmount(path); amount.Should().Be(0); } } public class GetLastLevel { [Fact] public void Should_ThrowException_When_NullPath() { Action action = () => PathHelper.GetLastLevel(null); action.Should().ThrowExactly(); } [Theory] [InlineData("some.path", "path")] [InlineData("path", "path")] [InlineData("", "")] [InlineData("#", "#")] [InlineData("some.path.#", "#")] [InlineData("some.path.#23", "#23")] [InlineData("some.#123.path.#23", "#23")] public void Should_GetLastLevel(string path, string expectedLastLevel) { var lastLevel = PathHelper.GetLastLevel(path); lastLevel.Should().Be(expectedLastLevel); } } public class IsValidAsPath { [Fact] public void Should_ReturnFalse_When_NullPath() { var isValidAsPath = PathHelper.IsValidAsPath(null); isValidAsPath.Should().Be(false); } [Theory] [MemberData(nameof(PathTestData.ValidPaths), MemberType = typeof(PathTestData))] public void Should_ReturnTrue_For_ValidPaths(string path) { var isValidAsPath = PathHelper.IsValidAsPath(path); isValidAsPath.Should().Be(true); } [Theory] [MemberData(nameof(PathTestData.InvalidPaths), MemberType = typeof(PathTestData))] public void Should_ReturnFalse_For_InvalidPaths(string path) { var isValidAsPath = PathHelper.IsValidAsPath(path); isValidAsPath.Should().Be(false); } } public class NormalizePath { [Fact] public void Should_ReturnSingleSpace_When_NullPath() { var normalized = PathHelper.NormalizePath(null); normalized.Should().Be(" "); PathHelper.IsValidAsPath(normalized).Should().BeTrue(); } [Theory] [MemberData(nameof(PathTestData.NormalizePath.TrimmingInitialAngleBracts), MemberType = typeof(PathTestData.NormalizePath))] [MemberData(nameof(PathTestData.NormalizePath.DotsTrimmingAndSquashing), MemberType = typeof(PathTestData.NormalizePath))] public void Should_NormalizeInvalidPaths(string path, string expectedNormalized) { var normalized = PathHelper.NormalizePath(path); normalized.Should().Be(expectedNormalized); PathHelper.IsValidAsPath(normalized).Should().BeTrue(); } [Theory] [InlineData("path1.path2")] [InlineData("path1.path2.path3")] [InlineData("path 1 . path2 . path3")] [MemberData(nameof(PathTestData.ValidPaths), MemberType = typeof(PathTestData))] public void Should_LeaveAsIs_If_PathIsValid(string path) { var isValidAsPath = PathHelper.IsValidAsPath(path); isValidAsPath.Should().Be(true); } } } } ================================================ FILE: tests/Validot.Tests.Unit/PathTestData.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using System.Globalization; using System.Linq; public class PathTestData { public static IEnumerable InvalidPaths() { yield return new[] { "segment." }; yield return new[] { ".path.segment" }; yield return new[] { "<.segment" }; yield return new[] { "<.path.segment" }; yield return new[] { "path.segment.another.segment." }; yield return new[] { "<." }; yield return new[] { ". " }; yield return new[] { ".." }; yield return new[] { "path..segment" }; yield return new[] { "path.segment..." }; yield return new[] { "..path.segment" }; yield return new[] { "path.segment...another.segment." }; yield return new[] { ".\t" }; } public static IEnumerable ValidPaths() { yield return new[] { "segment" }; yield return new[] { "path.segment" }; yield return new[] { " ResolvePath_AllCases() { var cases = new[] { ResolvePath.SimpleRelativePath(), ResolvePath.RelativePathContainsMoreLevelsDown(), ResolvePath.UncommonCharacters(), ResolvePath.RelativePathGoesLevelUp(), ResolvePath.RelativePathIsEmpty_And_GoesLevelUp(), ResolvePath.RelativePathGoesLevelUp_And_ExceedsMinimumLevel(), ResolvePath.ToSamePath() }; foreach (var @case in cases) { foreach (var set in @case) { yield return set; } } } public class ResolvePath { public static IEnumerable ToSamePath() { yield return new[] { "base.path", "", "base.path" }; yield return new[] { "base", "", "base" }; yield return new[] { "", "", "" }; } public static IEnumerable SimpleRelativePath() { yield return new[] { "base.path", "newSegment", "base.path.newSegment" }; yield return new[] { "base", "newSegment", "base.newSegment" }; } public static IEnumerable RelativePathContainsMoreLevelsDown() { yield return new[] { "base.path", "new.segment", "base.path.new.segment" }; yield return new[] { "base.path", "new.segment.value", "base.path.new.segment.value" }; } public static IEnumerable UncommonCharacters() { yield return new[] { "base.path", "n@ew.@eg!.me-+!@!n!t", "base.path.n@ew.@eg!.me-+!@!n!t" }; yield return new[] { "base.path", "new. .value", "base.path.new. .value" }; yield return new[] { "base. ", "new. .value", "base. .new. .value" }; yield return new[] { "base", " ", "base. " }; yield return new[] { "base", "< ", " " }; yield return new[] { "", "< ", " " }; yield return new[] { "base", " RelativePathGoesLevelUp() { yield return new[] { "base", " RelativePathIsEmpty_And_GoesLevelUp() { yield return new[] { "base", "<", "" }; yield return new[] { "base.path", "<", "base" }; yield return new[] { "base.path", "<<", "" }; } public static IEnumerable RelativePathGoesLevelUp_And_ExceedsMinimumLevel() { yield return new[] { "base", "<< GetWithIndexes_AllCases() { var cases = new[] { GetWithIndexes.CommonCases(), GetWithIndexes.TrickyCases(), GetWithIndexes.LargeIndexes() }; foreach (var @case in cases) { foreach (var set in @case) { yield return set; } } } public class GetWithIndexes { public static IEnumerable CommonCases() { yield return new object[] { "path.#", GetIndexesStack(1), "path.#0" }; yield return new object[] { "path.#.segment", GetIndexesStack(1), "path.#0.segment" }; yield return new object[] { "path.#.segment.#.another.path", GetIndexesStack(2), "path.#0.segment.#1.another.path" }; yield return new object[] { "path.#.#.segment", GetIndexesStack(2), "path.#0.#1.segment" }; yield return new object[] { "#", GetIndexesStack(1), "#0" }; yield return new object[] { "#.#", GetIndexesStack(2), "#0.#1" }; yield return new object[] { "#.#.test.#.#.path.#.#", GetIndexesStack(6), "#0.#1.test.#2.#3.path.#4.#5" }; } public static IEnumerable TrickyCases() { yield return new object[] { "##.#.#", GetIndexesStack(2), "##.#0.#1" }; yield return new object[] { "##.##.##", GetIndexesStack(2), "##.##.##" }; yield return new object[] { "####", GetIndexesStack(4), "####" }; yield return new object[] { "#a.#.b#.#", GetIndexesStack(2), "#a.#0.b#.#1" }; yield return new object[] { "# . #.#. #", GetIndexesStack(1), "# . #.#0. #" }; yield return new object[] { "", GetIndexesStack(2), "" }; } public static IEnumerable LargeIndexes() { Func process = m => (1 + m) * 100; yield return new object[] { "path.#", GetIndexesStack(1, process), "path.#100" }; yield return new object[] { "path.#.segment", GetIndexesStack(1, process), "path.#100.segment" }; yield return new object[] { "path.#.segment.#.another.path", GetIndexesStack(2, process), "path.#100.segment.#200.another.path" }; yield return new object[] { "path.#.#.segment", GetIndexesStack(2, process), "path.#100.#200.segment" }; yield return new object[] { "#", GetIndexesStack(1, process), "#100" }; yield return new object[] { "#.#", GetIndexesStack(2, process), "#100.#200" }; yield return new object[] { "#.#.test.#.#.path.#.#", GetIndexesStack(6, process), "#100.#200.test.#300.#400.path.#500.#600" }; } private static Stack GetIndexesStack(int count, Func process = null) { var stack = new Stack(); foreach (var i in Enumerable.Range(0, count).Select(m => process != null ? process(m) : m)) { stack.Push(i.ToString(CultureInfo.InvariantCulture)); } return stack; } } public class GetIndexesAmount { public static IEnumerable NoIndexes() { yield return new object[] { "" }; yield return new object[] { "path.segment" }; yield return new object[] { "path.segment.new" }; } public static IEnumerable InvalidIndexes() { yield return new object[] { "# " }; yield return new object[] { " #" }; yield return new object[] { " #1.path.##" }; yield return new object[] { "path.#1d.#x.segm##ent" }; yield return new object[] { "path. 1#. # .segm##ent.# " }; } public static IEnumerable WeirdIndexes() { yield return new object[] { "#1.#", 2 }; yield return new object[] { "#.path.#1", 2 }; yield return new object[] { "path.#1.#.segm##ent", 2 }; } public static IEnumerable ResolvedIndexes() { yield return new object[] { "#1", 1 }; yield return new object[] { "path.#1", 1 }; yield return new object[] { "path.#1.segment", 1 }; yield return new object[] { "path.#1.#2.segment", 2 }; yield return new object[] { "path.#1.#2.segment.#3", 3 }; yield return new object[] { "path.#1.#2.#3.segment", 3 }; yield return new object[] { "path.#1.#2.#3.segment.#4.#5.#6", 6 }; } public static IEnumerable PlaceholdersIndexes() { yield return new object[] { "#1", 1 }; yield return new object[] { "path.#1", 1 }; yield return new object[] { "path.#1.segment", 1 }; yield return new object[] { "path.#1.#2.segment", 2 }; yield return new object[] { "path.#1.#2.segment.#3", 3 }; yield return new object[] { "path.#1.#2.#3.segment", 3 }; yield return new object[] { "path.#1.#2.#3.segment.#4.#5.#6", 6 }; } } public class NormalizePath { public static IEnumerable DotsTrimmingAndSquashing() { yield return new object[] { "path1..path2", "path1.path2" }; yield return new object[] { "path1.......path2", "path1.path2" }; yield return new object[] { "path1.......path2..path3", "path1.path2.path3" }; yield return new object[] { "..path1.......path2..path3.", "path1.path2.path3" }; yield return new object[] { ".p..a...t....h.....", "p.a.t.h" }; yield return new object[] { ".path", "path" }; yield return new object[] { "....path", "path" }; yield return new object[] { ".path.", "path" }; yield return new object[] { "...path...", "path" }; yield return new object[] { ".p.a.t.h.", "p.a.t.h" }; yield return new object[] { "...p.a.t.h...", "p.a.t.h" }; } public static IEnumerable TrimmingInitialAngleBracts() { yield return new object[] { ">(), new Dictionary(), Substitute.For()); } [Fact] public void AnyErrors_Should_BeFalse_When_NoErrors() { var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), Substitute.For()); validationResult.AnyErrors.Should().BeFalse(); } [Fact] public void AnyErrors_Should_BeTrue_When_ResultContainsErrors() { var resultErrors = new Dictionary>(); resultErrors.Add("path", new List() { 1 }); var validationResult = new ValidationResult(resultErrors, new Dictionary(), Substitute.For()); validationResult.AnyErrors.Should().BeTrue(); } [Fact] public void NoErrorsResult_Should_BeResultWithoutErrors() { ValidationResult.NoErrorsResult.AnyErrors.Should().BeFalse(); ValidationResult.NoErrorsResult.Paths.Should().BeEmpty(); ValidationResult.NoErrorsResult.TranslationNames.Should().BeEmpty(); ValidationResult.NoErrorsResult.Codes.Should().BeEmpty(); ValidationResult.NoErrorsResult.CodeMap.Should().BeEmpty(); ValidationResult.NoErrorsResult.MessageMap.Should().BeEmpty(); ValidationResult.NoErrorsResult.GetErrorOutput().Should().BeEmpty(); ValidationResult.NoErrorsResult.GetTranslatedMessageMap(null).Should().BeEmpty(); ValidationResult.NoErrorsResult.GetTranslatedMessageMap("English").Should().BeEmpty(); } public static IEnumerable Paths_Should_ReturnAllPaths_Data() { yield return new object[] { new Dictionary>(), Array.Empty() }; yield return new object[] { new Dictionary>() { ["test1"] = new List(), }, new[] { "test1" } }; yield return new object[] { new Dictionary>() { ["test1"] = new List(), ["test2"] = new List(), ["nested.test3"] = new List() }, new[] { "test1", "test2", "nested.test3" } }; } [Theory] [MemberData(nameof(Paths_Should_ReturnAllPaths_Data))] public void Paths_Should_ReturnAllPaths(Dictionary> resultsErrors, IReadOnlyList expectedPaths) { var validationResult = new ValidationResult(resultsErrors, new Dictionary(), Substitute.For()); validationResult.Paths.Should().NotBeNull(); validationResult.Paths.Should().HaveCount(expectedPaths.Count); foreach (var expectedPath in expectedPaths) { validationResult.Paths.Should().Contain(expectedPath); } } public class TranslationNames { [Fact] public void Should_Return_TranslationNames_FromMessageService() { var messageService = Substitute.For(); var translationNames = new[] { "translation1", "translation2" }; messageService.TranslationNames.Returns(translationNames); var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), messageService); validationResult.TranslationNames.Should().BeSameAs(translationNames); } [Fact] public void Should_Return_EmptyTranslationNames_When_NullTranslationName_InMessageService() { var messageService = Substitute.For(); messageService.TranslationNames.Returns(null as IReadOnlyList); var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), null); validationResult.TranslationNames.Should().BeEmpty(); } [Fact] public void Should_Return_EmptyTranslationNames_When_NullMessageService() { var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), null); validationResult.TranslationNames.Should().BeEmpty(); } } public class GetTranslatedMessageMap { [Fact] public void Should_Return_Messages_FromMessageService_WithDefaultTranslation_WhenNullTranslationName() { var messageService = Substitute.For(); var errorMessages = new Dictionary> { ["path1"] = new[] { "message11" }, ["path2"] = new[] { "message12", "message22" } }; var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 } }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)).Returns(errorMessages); var validationResult = new ValidationResult(resultErrors, new Dictionary(), messageService); var resultErrorMessages = validationResult.GetTranslatedMessageMap(null); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)); messageService.ReceivedWithAnyArgs(1).GetMessages(default); resultErrorMessages.Should().BeSameAs(errorMessages); } [Fact] public void Should_Return_Messages_FromMessageService_WithSpecifiedTranslation() { var messageService = Substitute.For(); var errorMessages1 = new Dictionary> { ["path1"] = new[] { "message11" }, ["path2"] = new[] { "message12", "message22" } }; var errorMessages2 = new Dictionary> { ["path1"] = new[] { "MESSAGE11" }, ["path2"] = new[] { "MESSAGE12", "MESSAGE22" } }; var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 } }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation1")).Returns(errorMessages1); messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation2")).Returns(errorMessages2); var validationResult = new ValidationResult(resultErrors, new Dictionary(), messageService); var resultErrorMessages = validationResult.GetTranslatedMessageMap("translation2"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation2")); messageService.ReceivedWithAnyArgs(1).GetMessages(default); resultErrorMessages.Should().BeSameAs(errorMessages2); } [Fact] public void Should_Return_EmptyMessageMap_When_Valid() { var messageService = Substitute.For(); var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), null); var resultErrorMessages = validationResult.GetTranslatedMessageMap(null); messageService.DidNotReceiveWithAnyArgs().GetMessages(default); resultErrorMessages.Should().NotBeNull(); resultErrorMessages.Should().BeEmpty(); } } public class CodeMap { [Fact] public void Should_Return_EmptyCodeMap_When_Valid() { var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), null); validationResult.CodeMap.Should().NotBeNull(); validationResult.CodeMap.Should().BeEmpty(); } [Fact] public void Should_Return_AllCodes() { var resultsErrors = new Dictionary>() { [""] = new List() { 1 }, ["test1"] = new List() { 1, 2, 3 }, ["test2"] = new List() { 2, 4 }, ["test3"] = new List() { 3 }, }; var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "Message1", } }, [2] = new Error() { Codes = new[] { "Message2", } }, [3] = new Error() { Codes = new[] { "Message3", } }, [4] = new Error() { Codes = new[] { "Message41", "Message42" } }, }; var validationResult = new ValidationResult(resultsErrors, errorRegistry, Substitute.For()); validationResult.CodeMap.Should().NotBeNull(); validationResult.CodeMap.Should().HaveCount(4); validationResult.CodeMap.Keys.Should().Contain(""); validationResult.CodeMap[""].Should().HaveCount(1); validationResult.CodeMap[""].Should().Contain("Message1"); validationResult.CodeMap.Keys.Should().Contain("test1"); validationResult.CodeMap["test1"].Should().HaveCount(3); validationResult.CodeMap["test1"].Should().Contain("Message1", "Message2", "Message3"); validationResult.CodeMap.Keys.Should().Contain("test2"); validationResult.CodeMap["test2"].Should().HaveCount(3); validationResult.CodeMap["test2"].Should().Contain("Message2", "Message41", "Message42"); validationResult.CodeMap.Keys.Should().Contain("test3"); validationResult.CodeMap["test3"].Should().HaveCount(1); validationResult.CodeMap["test3"].Should().Contain("Message3"); } public static IEnumerable Should_Return_AllCodes_MoreExamples_Data() { var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", } }, [2] = new Error() { Codes = new[] { "CODE2", } }, [3] = new Error() { Codes = new[] { "CODE3", } }, [4] = new Error() { Codes = new[] { "CODE41", "CODE42" } }, [5] = new Error() { }, [6] = new Error() { Codes = new[] { "CODE61", "CODE62", "CODE63" } }, [10] = new Error() { Codes = new[] { "CODE1", "CODE2" } }, }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, }, errorRegistry, new Dictionary>() { ["test1"] = new[] { "CODE1" } } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, ["test2"] = new List() { 2, 4 }, ["test3"] = new List() { 3, 4 }, }, errorRegistry, new Dictionary>() { ["test1"] = new[] { "CODE1" }, ["test2"] = new[] { "CODE2", "CODE41", "CODE42" }, ["test3"] = new[] { "CODE3", "CODE41", "CODE42" } } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 5 }, ["test2"] = new List() { 2, 5 }, ["test3"] = new List() { 3, 5 }, ["test4"] = new List() { 6, 5 }, }, errorRegistry, new Dictionary>() { ["test2"] = new[] { "CODE2", }, ["test3"] = new[] { "CODE3", }, ["test4"] = new[] { "CODE61", "CODE62", "CODE63", }, } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 5, 10 }, ["test2"] = new List() { 2, 5 }, ["test3"] = new List() { 3, 6 }, ["test4"] = new List() { 5 }, ["test5"] = new List() { 5 }, }, errorRegistry, new Dictionary>() { ["test1"] = new[] { "CODE1", "CODE2" }, ["test2"] = new[] { "CODE2", }, ["test3"] = new[] { "CODE3", "CODE61", "CODE62", "CODE63", }, } }; } [Theory] [MemberData(nameof(Should_Return_AllCodes_MoreExamples_Data))] public void Should_Return_AllCodes_MoreExamples(Dictionary> resultsErrors, Dictionary errorRegistry, Dictionary> expectedCodes) { var validationResult = new ValidationResult(resultsErrors, errorRegistry, Substitute.For()); validationResult.CodeMap.Should().NotBeNull(); validationResult.CodeMap.Should().HaveCount(expectedCodes.Count); foreach (var pair in expectedCodes) { validationResult.CodeMap.Keys.Should().Contain(pair.Key); validationResult.CodeMap[pair.Key].Should().HaveCount(pair.Value.Count); validationResult.CodeMap[pair.Key].Should().Contain(pair.Value); } } } public class Codes { [Fact] public void Should_ReturnAllCodesFromErrors_WithoutDuplicates() { var resultsErrors = new Dictionary>() { ["test1"] = new List() { 1, 2, 3 }, ["test2"] = new List() { 2, 4 }, }; var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", } }, [2] = new Error() { Codes = new[] { "CODE2", } }, [3] = new Error() { Codes = new[] { "CODE3", } }, [4] = new Error() { Codes = new[] { "CODE41", "CODE42" } }, }; var validationResult = new ValidationResult(resultsErrors, errorRegistry, Substitute.For()); validationResult.Codes.Should().NotBeNull(); validationResult.Codes.Should().HaveCount(5); validationResult.Codes.Should().Contain("CODE1"); validationResult.Codes.Should().Contain("CODE2"); validationResult.Codes.Should().Contain("CODE3"); validationResult.Codes.Should().Contain("CODE41"); validationResult.Codes.Should().Contain("CODE42"); } public static IEnumerable Should_ReturnAllCodesFromErrors_WithoutDuplicates_MoreExamples_Data() { var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", } }, [2] = new Error() { Codes = new[] { "CODE2", } }, [3] = new Error() { Codes = new[] { "CODE3", } }, [4] = new Error() { Codes = new[] { "CODE41", "CODE42" } }, [5] = new Error() { }, [6] = new Error() { Codes = new[] { "CODE61", "CODE62", "CODE63" } }, [10] = new Error() { Codes = new[] { "CODE1", "CODE2" } }, }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, }, errorRegistry, new[] { "CODE1" } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1, 1, 1 }, }, errorRegistry, new[] { "CODE1" } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, ["test2"] = new List() { 1 }, ["test3"] = new List() { 1 }, }, errorRegistry, new[] { "CODE1" } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, ["test2"] = new List() { 2, 4 }, ["test3"] = new List() { 3, 4 }, }, errorRegistry, new[] { "CODE1", "CODE2", "CODE3", "CODE41", "CODE42" } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 5 }, ["test2"] = new List() { 2, 5 }, ["test3"] = new List() { 3, 5 }, ["test4"] = new List() { 6, 5 }, }, errorRegistry, new[] { "CODE2", "CODE3", "CODE61", "CODE62", "CODE63" } }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 5, 10 }, ["test2"] = new List() { 2, 5 }, ["test3"] = new List() { 3, 6 }, ["test4"] = new List() { 5 }, ["test5"] = new List() { 5 }, }, errorRegistry, new[] { "CODE1", "CODE2", "CODE3", "CODE61", "CODE62", "CODE63" } }; } [Theory] [MemberData(nameof(Should_ReturnAllCodesFromErrors_WithoutDuplicates_MoreExamples_Data))] public void Should_ReturnAllCodesFromErrors_WithoutDuplicates_MoreExamples(Dictionary> resultsErrors, Dictionary errorRegistry, IReadOnlyList expectedCodes) { var validationResult = new ValidationResult(resultsErrors, errorRegistry, Substitute.For()); validationResult.Codes.Should().NotBeNull(); validationResult.Codes.Should().HaveCount(expectedCodes.Count); validationResult.Codes.Should().Contain(expectedCodes); expectedCodes.Should().Contain(validationResult.Codes); } [Fact] public void Should_ReturnEmptyList_When_Valid() { var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), Substitute.For()); validationResult.Codes.Should().NotBeNull(); validationResult.Codes.Should().BeEmpty(); } } public class GetErrorOutput { [Fact] public void Should_ReturnErrorOutput() { var resultsErrors = new Dictionary>() { ["test1"] = new List() { 1, 2, 3 }, ["test2"] = new List() { 2, 4 }, }; var errorRegistry = new Dictionary() { [1] = new Error(), [2] = new Error(), [3] = new Error(), [4] = new Error(), }; var validationResult = new ValidationResult(resultsErrors, errorRegistry, Substitute.For()); var rawErrors = validationResult.GetErrorOutput(); rawErrors.Should().NotBeNull(); rawErrors.Keys.Should().HaveCount(2); rawErrors["test1"].Should().HaveCount(3); rawErrors["test1"].Should().Contain(x => ReferenceEquals(x, errorRegistry[1])); rawErrors["test1"].Should().Contain(x => ReferenceEquals(x, errorRegistry[2])); rawErrors["test1"].Should().Contain(x => ReferenceEquals(x, errorRegistry[3])); rawErrors["test2"].Should().HaveCount(2); rawErrors["test2"].Should().Contain(x => ReferenceEquals(x, errorRegistry[2])); rawErrors["test2"].Should().Contain(x => ReferenceEquals(x, errorRegistry[4])); } public static IEnumerable Should_ReturnErrorOutput_MoreExamples_Data() { var errorRegistry = new Dictionary() { [1] = new Error(), [2] = new Error(), [3] = new Error(), [4] = new Error(), }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, }, errorRegistry, new Dictionary>() { ["test1"] = new[] { errorRegistry[1] } }, }; yield return new object[] { new Dictionary>() { [""] = new List() { 4 }, }, errorRegistry, new Dictionary>() { [""] = new[] { errorRegistry[4] } }, }; yield return new object[] { new Dictionary>() { ["test1"] = new List(), }, errorRegistry, new Dictionary>() { ["test1"] = new IError[] { }, }, }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, ["test2"] = new List() { 2 }, ["test3"] = new List() { 3 }, ["test4"] = new List() { 4 }, }, errorRegistry, new Dictionary>() { ["test1"] = new[] { errorRegistry[1] }, ["test2"] = new[] { errorRegistry[2] }, ["test3"] = new[] { errorRegistry[3] }, ["test4"] = new[] { errorRegistry[4] }, }, }; yield return new object[] { new Dictionary>() { ["test1"] = new List() { 1 }, ["test2"] = new List() { 1, 2 }, ["test3"] = new List() { 1, 3 }, ["test4"] = new List() { 2, 3, 4 }, }, errorRegistry, new Dictionary>() { ["test1"] = new[] { errorRegistry[1] }, ["test2"] = new[] { errorRegistry[1], errorRegistry[2] }, ["test3"] = new[] { errorRegistry[1], errorRegistry[3] }, ["test4"] = new[] { errorRegistry[2], errorRegistry[3], errorRegistry[4] }, }, }; } [Theory] [MemberData(nameof(Should_ReturnErrorOutput_MoreExamples_Data))] public void Should_ReturnErrorOutput_MoreExamples(Dictionary> resultsErrors, Dictionary errorRegistry, IReadOnlyDictionary> expectedErrors) { var validationResult = new ValidationResult(resultsErrors, errorRegistry, Substitute.For()); var rawErrors = validationResult.GetErrorOutput(); rawErrors.Should().NotBeNull(); rawErrors.Keys.Should().HaveCount(resultsErrors.Count); foreach (var expectedErrorsPair in expectedErrors) { rawErrors.Keys.Should().Contain(expectedErrorsPair.Key); rawErrors[expectedErrorsPair.Key].Should().HaveCount(expectedErrorsPair.Value.Count); foreach (var error in expectedErrorsPair.Value) { rawErrors[expectedErrorsPair.Key].Should().Contain(x => ReferenceEquals(x, error)); } } } [Fact] public void Should_ReturnEmptyDictionary_When_Valid() { var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), Substitute.For()); var rawErrors = validationResult.GetErrorOutput(); rawErrors.Should().NotBeNull(); rawErrors.Should().BeEmpty(); } } public class ToStringTests { [Fact] public void Should_Return_NoErrorsString_When_Valid() { var validationResult = new ValidationResult(new Dictionary>(), new Dictionary(), null); var stringified = validationResult.ToString(); stringified.Should().Be("OK"); } [Fact] public void Should_Return_Messages_FromMessageService_WithDefaultTranslation() { var messageService = Substitute.For(); var errorMessages = new Dictionary> { ["path1"] = new[] { "message11" }, ["path2"] = new[] { "message12", "message22" } }; var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 } }; var errorsRegistry = new Dictionary() { [1] = new Error() }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)).Returns(errorMessages); var validationResult = new ValidationResult(resultErrors, errorsRegistry, messageService); validationResult.ToString().ShouldResultToStringHaveLines( ToStringContentType.Messages, "path1: message11", "path2: message12", "path2: message22"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)); messageService.ReceivedWithAnyArgs(1).GetMessages(default); } [Fact] public void Should_Return_Messages_FromMessageService_WithDefaultTranslation_And_Codes() { var messageService = Substitute.For(); var errorMessages = new Dictionary> { ["path1"] = new[] { "message11" }, ["path2"] = new[] { "message12", "message22" } }; var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 }, ["path2"] = new List() { 2, 3 }, ["path3"] = new List() { 1, 3 } }; var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", "CODE2" } }, [2] = new Error() { Codes = new[] { "CODE2", "CODE3" } }, [3] = new Error() { Codes = new[] { "CODE1", "CODE2", "CODE3", "CODE4" } } }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)).Returns(errorMessages); var validationResult = new ValidationResult(resultErrors, errorRegistry, messageService); validationResult.ToString().ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "CODE1, CODE2, CODE3, CODE4", "", "path1: message11", "path2: message12", "path2: message22"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)); messageService.ReceivedWithAnyArgs(1).GetMessages(default); } [Fact] public void Should_Return_Messages_FromMessageService_WithSpecifiedTranslation() { var messageService = Substitute.For(); var errorMessages1 = new Dictionary> { ["path1"] = new[] { "message11" }, ["path2"] = new[] { "message12", "message22" } }; var errorMessages2 = new Dictionary> { ["path1"] = new[] { "MESSAGE11" }, ["path2"] = new[] { "MESSAGE12", "MESSAGE22" } }; var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 } }; var errorRegistry = new Dictionary() { [1] = new Error() }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation1")).Returns(errorMessages1); messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation2")).Returns(errorMessages2); var validationResult = new ValidationResult(resultErrors, errorRegistry, messageService); validationResult.ToString("translation2").ShouldResultToStringHaveLines( ToStringContentType.Messages, "path1: MESSAGE11", "path2: MESSAGE12", "path2: MESSAGE22"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation2")); messageService.ReceivedWithAnyArgs(1).GetMessages(default); } [Fact] public void Should_Return_Messages_FromMessageService_WithSpecifiedTranslation_And_Codes() { var messageService = Substitute.For(); var errorMessages1 = new Dictionary> { ["path1"] = new[] { "message11" }, ["path2"] = new[] { "message12", "message22" } }; var errorMessages2 = new Dictionary> { ["path1"] = new[] { "MESSAGE11" }, ["path2"] = new[] { "MESSAGE12", "MESSAGE22" } }; var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 }, ["path2"] = new List() { 2, 3 }, ["path3"] = new List() { 1, 3 } }; var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", "CODE2" } }, [2] = new Error() { Codes = new[] { "CODE2", "CODE3" } }, [3] = new Error() { Codes = new[] { "CODE1", "CODE2", "CODE3", "CODE4" } } }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation1")).Returns(errorMessages1); messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation2")).Returns(errorMessages2); var validationResult = new ValidationResult(resultErrors, errorRegistry, messageService); validationResult.ToString("translation2").ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "CODE1, CODE2, CODE3, CODE4", "", "path1: MESSAGE11", "path2: MESSAGE12", "path2: MESSAGE22"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is("translation2")); messageService.ReceivedWithAnyArgs(1).GetMessages(default); } [Fact] public void Should_Return_Codes() { var messageService = Substitute.For(); var errorMessages = new Dictionary>(); var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 } }; var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", "CODE2" } } }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)).Returns(errorMessages); var validationResult = new ValidationResult(resultErrors, errorRegistry, messageService); validationResult.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "CODE1, CODE2"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)); messageService.ReceivedWithAnyArgs(1).GetMessages(default); } [Fact] public void Should_Return_Codes_WithoutDuplicates() { var messageService = Substitute.For(); var errorMessages = new Dictionary>(); var resultErrors = new Dictionary>() { ["path1"] = new List() { 1 }, ["path2"] = new List() { 2, 3 }, ["path3"] = new List() { 1, 3 } }; var errorRegistry = new Dictionary() { [1] = new Error() { Codes = new[] { "CODE1", "CODE2" } }, [2] = new Error() { Codes = new[] { "CODE2", "CODE3" } }, [3] = new Error() { Codes = new[] { "CODE1", "CODE2", "CODE3", "CODE4" } } }; messageService.GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)).Returns(errorMessages); var validationResult = new ValidationResult(resultErrors, errorRegistry, messageService); validationResult.ToString().ShouldResultToStringHaveLines( ToStringContentType.Codes, "CODE1, CODE2, CODE3, CODE4"); messageService.Received(1).GetMessages(Arg.Is>>(a => ReferenceEquals(a, resultErrors)), Arg.Is(null as string)); messageService.ReceivedWithAnyArgs(1).GetMessages(default); } // private static void ShouldHaveMessagesOnly(string validationResult, IReadOnlyList messages) => ShouldHaveCodesAndMessages(validationResult, null, messages); // // private static void ShouldHaveCodesOnly(string validationResult, IReadOnlyList codes) => ShouldHaveCodesAndMessages(validationResult, codes, null); // // private static void ShouldHaveCodesAndMessages(string validationResult, IReadOnlyList codes, IReadOnlyList messages) // { // validationResult.Should().NotBeNullOrEmpty(); // // var anyCodes = codes?.Any() == true; // var anyMessages = messages?.Any() == true; // // if (!anyCodes && !anyMessages) // { // validationResult.Should().Be("(no error output)"); // // return; // } // // if (anyCodes) // { // if (anyMessages) // { // validationResult.Should().Contain(Environment.NewLine); // } // else // { // validationResult.Should().NotContain(Environment.NewLine); // } // // var codesLine = anyMessages // ? validationResult.Substring(0, validationResult.IndexOf(Environment.NewLine, StringComparison.Ordinal)) // : validationResult; // // var extractedCodes = codesLine.Split(new[] { ", " }, StringSplitOptions.None); // // extractedCodes.Should().HaveCount(codes.Count); // extractedCodes.Should().Contain(codes); // codes.Should().Contain(extractedCodes, because: "(reversed)"); // } // // if (anyMessages) // { // string messagesPart; // // if (anyCodes) // { // messagesPart = validationResult.Substring(validationResult.IndexOf(Environment.NewLine, StringComparison.Ordinal)); // // messagesPart.Should().StartWith(Environment.NewLine); // messagesPart = messagesPart.Substring(Environment.NewLine.Length); // // messagesPart.Should().StartWith(Environment.NewLine); // messagesPart = messagesPart.Substring(Environment.NewLine.Length); // } // else // { // messagesPart = validationResult; // } // // var lines = messagesPart.Split(new[] { Environment.NewLine }, StringSplitOptions.None); // // lines.Should().Contain(messages); // messages.Should().Contain(lines, because: "(reversed)"); // lines.Should().HaveCount(messages.Count); // } // } } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/BoolRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules { using Validot.Testing; using Validot.Translations; using Xunit; public class BoolRulesTests { [Theory] [InlineData(true, true)] [InlineData(false, false)] public void True_Should_CollectError(bool value, bool expectedIsValid) { Tester .TestSingleRule( value, m => m.True(), expectedIsValid, MessageKey.BoolType.True); } [Theory] [InlineData(true, true)] [InlineData(false, false)] public void NullableTrue_Should_CollectError(bool value, bool expectedIsValid) { Tester .TestSingleRule( value, m => m.True(), expectedIsValid, MessageKey.BoolType.True); } [Theory] [InlineData(false, true)] [InlineData(true, false)] public void False_Should_CollectError(bool value, bool expectedIsValid) { Tester.TestSingleRule( value, m => m.False(), expectedIsValid, MessageKey.BoolType.False); } [Theory] [InlineData(false, true)] [InlineData(true, false)] public void NullableFalse_Should_CollectError(bool value, bool expectedIsValid) { Tester.TestSingleRule( value, m => m.False(), expectedIsValid, MessageKey.BoolType.False); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/CharRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules { using Validot.Testing; using Validot.Translations; using Xunit; public class CharRulesTests { [Theory] [InlineData('a', 'a', true)] [InlineData('A', 'a', true)] [InlineData('a', 'A', true)] [InlineData('A', 'A', true)] [InlineData('A', 'b', false)] [InlineData('a', 'B', false)] [InlineData('a', 'b', false)] [InlineData('A', 'B', false)] [InlineData('Ż', 'Ż', true)] [InlineData('ć', 'Ć', true)] [InlineData('Ą', 'ó', false)] public void EqualIgnoreCase_Should_CollectError(char value, char test, bool shouldBeValid) { Tester.TestSingleRule( value, m => m.EqualToIgnoreCase(test), shouldBeValid, MessageKey.CharType.EqualToIgnoreCase, Arg.Text("value", test)); } [Theory] [InlineData('a', 'a', true)] [InlineData('A', 'a', true)] [InlineData('a', 'A', true)] [InlineData('A', 'A', true)] [InlineData('A', 'b', false)] [InlineData('a', 'B', false)] [InlineData('a', 'b', false)] [InlineData('A', 'B', false)] [InlineData('Ż', 'Ż', true)] [InlineData('ć', 'Ć', true)] [InlineData('Ą', 'ó', false)] public void EqualIgnoreCase_Nullable_Should_CollectError(char value, char test, bool shouldBeValid) { Tester.TestSingleRule( value, m => m.EqualToIgnoreCase(test), shouldBeValid, MessageKey.CharType.EqualToIgnoreCase, Arg.Text("value", test)); } [Theory] [InlineData('a', 'a', false)] [InlineData('A', 'a', false)] [InlineData('a', 'A', false)] [InlineData('A', 'A', false)] [InlineData('A', 'b', true)] [InlineData('a', 'B', true)] [InlineData('a', 'b', true)] [InlineData('A', 'B', true)] [InlineData('Ż', 'Ż', false)] [InlineData('ć', 'Ć', false)] [InlineData('Ą', 'ó', true)] public void NotEqualIgnoreCase_Should_CollectError(char value, char test, bool shouldBeValid) { Tester.TestSingleRule( value, m => m.NotEqualToIgnoreCase(test), shouldBeValid, MessageKey.CharType.NotEqualToIgnoreCase, Arg.Text("value", test)); } [Theory] [InlineData('a', 'a', false)] [InlineData('A', 'a', false)] [InlineData('a', 'A', false)] [InlineData('A', 'A', false)] [InlineData('A', 'b', true)] [InlineData('a', 'B', true)] [InlineData('a', 'b', true)] [InlineData('A', 'B', true)] [InlineData('Ż', 'Ż', false)] [InlineData('ć', 'Ć', false)] [InlineData('Ą', 'ó', true)] public void NotEqualIgnoreCase_Nullable_Should_CollectError(char value, char test, bool shouldBeValid) { Tester.TestSingleRule( value, m => m.NotEqualToIgnoreCase(test), shouldBeValid, MessageKey.CharType.NotEqualToIgnoreCase, Arg.Text("value", test)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/ArrayRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class ArrayRulesTests { private static readonly Func Convert = array => array; public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(int[] model, int size, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(int[] model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(int[] model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(int[] model, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(int[] model, int min, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(int[] model, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/BaseCollectionRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Validot.Testing; using Validot.Translations; using Xunit; public class BaseCollectionRulesTests { private static readonly Func Convert = array => new CustomCollection(array); public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(CustomCollection member, int size, bool expectedIsValid) { Tester.TestSingleRule( member, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(CustomCollection member, bool expectedIsValid) { Tester.TestSingleRule( member, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(CustomCollection member, bool expectedIsValid) { Tester.TestSingleRule( member, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(CustomCollection member, int max, bool expectedIsValid) { Tester.TestSingleRule( member, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(CustomCollection member, int min, bool expectedIsValid) { Tester.TestSingleRule( member, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(CustomCollection member, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( member, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } public class CustomCollection : IEnumerable { private readonly List _source; public CustomCollection(IEnumerable source) { _source = source.ToList(); } public IEnumerator GetEnumerator() { return _source.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/CollectionsTestData.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; public static class CollectionsTestData { public static IEnumerable ExactCollectionSize_Should_CollectError_Data(Func convert) { yield return new object[] { convert(Array.Empty()), 0, true }; yield return new object[] { convert(new[] { 1 }), 1, true }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 10, true }; yield return new object[] { convert(Array.Empty()), 5, false }; yield return new object[] { convert(new[] { 1 }), 0, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 5, false }; } public static IEnumerable NotEmptyCollection_Should_CollectError_Data(Func convert) { yield return new object[] { convert(Array.Empty()), false }; yield return new object[] { convert(new[] { 1 }), true }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), true }; } public static IEnumerable EmptyCollection_Should_CollectError_Data(Func convert) { yield return new object[] { convert(Array.Empty()), true }; yield return new object[] { convert(new[] { 1 }), false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), false }; } public static IEnumerable MaxCollectionSize_Should_CollectError_Data(Func convert) { yield return new object[] { convert(Array.Empty()), 0, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 4, true }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 10, true }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), int.MaxValue, true }; yield return new object[] { convert(new[] { 1 }), 0, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4 }), 3, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 5, false }; } public static IEnumerable MinCollectionSize_Should_CollectError_Data(Func convert) { yield return new object[] { convert(Array.Empty()), 0, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 1, true }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 5, true }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 0, true }; yield return new object[] { convert(Array.Empty()), 1, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4 }), 5, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), int.MaxValue, false }; } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data(Func convert) { yield return new object[] { convert(Array.Empty()), 0, 1, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 0, 3, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 1, 3, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 2, 3, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 3, 3, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 3, 4, true }; yield return new object[] { convert(new[] { 1, 2, 3 }), 3, int.MaxValue, true }; yield return new object[] { convert(Array.Empty()), 1, 2, false }; yield return new object[] { convert(new[] { 1, 2, 3 }), 4, 10, false }; yield return new object[] { convert(new[] { 1, 2, 3 }), 4, int.MaxValue, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 1, 9, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 5, 5, false }; yield return new object[] { convert(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }), 11, int.MaxValue, false }; } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/IEnumerableRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class IEnumerableRulesTests { private static readonly Func> Convert = array => array; public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(IEnumerable model, int size, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit>( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(IEnumerable model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(IEnumerable model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(IEnumerable model, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(IEnumerable model, int min, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit>( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit>( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(IEnumerable model, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/IListRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; using System.Linq; using Validot.Testing; using Validot.Translations; using Xunit; public class IListRulesTests { private static readonly Func> Convert = array => array.ToList(); public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(IList model, int size, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit>( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(IList model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(IList model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(IList model, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(IList model, int min, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit>( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit>( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(IList model, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/IReadOnlyCollectionRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; using System.Linq; using Validot.Testing; using Validot.Translations; using Xunit; public class IReadOnlyCollectionRulesTests { private static readonly Func> Convert = array => array.ToList(); public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(IReadOnlyCollection model, int size, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit>( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(IReadOnlyCollection model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(IReadOnlyCollection model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(IReadOnlyCollection model, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(IReadOnlyCollection model, int min, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit>( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit>( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(IReadOnlyCollection model, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/IReadOnlyListRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; using System.Linq; using Validot.Testing; using Validot.Translations; using Xunit; public class IReadOnlyListRulesTests { private static readonly Func> Convert = array => array.ToList(); public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(IReadOnlyList model, int size, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit>( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(IReadOnlyList model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(IReadOnlyList model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(IReadOnlyList model, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(IReadOnlyList model, int min, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit>( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit>( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(IReadOnlyList model, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Collections/ListRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Collections { using System; using System.Collections.Generic; using System.Linq; using Validot.Testing; using Validot.Translations; using Xunit; public class ListRulesTests { private static readonly Func> Convert = array => array.ToList(); public static IEnumerable ExactCollectionSize_Should_CollectError_Data() { return CollectionsTestData.ExactCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(ExactCollectionSize_Should_CollectError_Data))] public void ExactCollectionSize_Should_CollectError(List model, int size, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.ExactCollectionSize(size), expectedIsValid, MessageKey.Collections.ExactCollectionSize, Arg.Number("size", size)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int size) { Tester.TestExceptionOnInit>( s => s.ExactCollectionSize(size), typeof(ArgumentOutOfRangeException)); } public static IEnumerable NotEmptyCollection_Should_CollectError_Data() { return CollectionsTestData.NotEmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(NotEmptyCollection_Should_CollectError_Data))] public void NotEmptyCollection_Should_CollectError(List model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEmptyCollection(), expectedIsValid, MessageKey.Collections.NotEmptyCollection); } public static IEnumerable EmptyCollection_Should_CollectError_Data() { return CollectionsTestData.EmptyCollection_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(EmptyCollection_Should_CollectError_Data))] public void EmptyCollection_Should_CollectError(List model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EmptyCollection(), expectedIsValid, MessageKey.Collections.EmptyCollection); } public static IEnumerable MaxCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MaxCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MaxCollectionSize_Should_CollectError_Data))] public void MaxCollectionSize_Should_CollectError(List model, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MaxCollectionSize(max), expectedIsValid, MessageKey.Collections.MaxCollectionSize, Arg.Number("max", max)); } public static IEnumerable MinCollectionSize_Should_CollectError_Data() { return CollectionsTestData.MinCollectionSize_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(MinCollectionSize_Should_CollectError_Data))] public void MinCollectionSize_Should_CollectError(List model, int min, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.MinCollectionSize(min), expectedIsValid, MessageKey.Collections.MinCollectionSize, Arg.Number("min", min)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int min) { Tester.TestExceptionOnInit>( s => s.MinCollectionSize(min), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxCollectionSize_Should_ThrowException_When_NegativeCollectionSize(int max) { Tester.TestExceptionOnInit>( s => s.MaxCollectionSize(max), typeof(ArgumentOutOfRangeException)); } public static IEnumerable CollectionSizeBetween_Should_CollectError_Data() { return CollectionsTestData.CollectionSizeBetween_Should_CollectError_Data(Convert); } [Theory] [MemberData(nameof(CollectionSizeBetween_Should_CollectError_Data))] public void CollectionSizeBetween_Should_CollectError(List model, int min, int max, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.CollectionSizeBetween(min, max), expectedIsValid, MessageKey.Collections.CollectionSizeBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MaxCollectionSizeIsNegative(int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(0, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void CollectionSizeBetween_Should_ThrowException_When_MinCollectionSizeIsNegative(int min) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, 10), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(1, 0)] [InlineData(20, 0)] [InlineData(int.MaxValue, 1)] public void CollectionSizeBetween_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit>( s => s.CollectionSizeBetween(min, max), typeof(ArgumentException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/GuidRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules { using System; using System.Collections.Generic; using Validot.Rules; using Validot.Testing; using Validot.Translations; using Xunit; public class GuidRulesTests { public static IEnumerable EqualTo_Should_CollectError_Data() { yield return new object[] { new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"), new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"), true }; yield return new object[] { new Guid("e2ce1f3b-17e5-412e-923b-6b4e268f31aa"), new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"), false }; } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(Guid memberValue, Guid argValue, bool expectedIsValid) { Tester.TestSingleRule( memberValue, m => m.EqualTo(argValue), expectedIsValid, MessageKey.GuidType.EqualTo, Arg.GuidValue("value", argValue)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(Guid memberValue, Guid argValue, bool expectedIsValid) { Tester.TestSingleRule( memberValue, m => m.EqualTo(argValue), expectedIsValid, MessageKey.GuidType.EqualTo, Arg.GuidValue("value", argValue)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { yield return new object[] { new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"), new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"), false }; yield return new object[] { new Guid("e2ce1f3b-17e5-412e-923b-6b4e268f31aa"), new Guid("c2ce1f3b-17e5-412e-923b-6b4e268f31aa"), true }; } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(Guid memberValue, Guid argValue, bool expectedIsValid) { Tester.TestSingleRule( memberValue, m => m.NotEqualTo(argValue), expectedIsValid, MessageKey.GuidType.NotEqualTo, Arg.GuidValue("value", argValue)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(Guid memberValue, Guid argValue, bool expectedIsValid) { Tester.TestSingleRule( memberValue, m => m.NotEqualTo(argValue), expectedIsValid, MessageKey.GuidType.NotEqualTo, Arg.GuidValue("value", argValue)); } public static IEnumerable NotEmpty_Should_CollectError_Data() { yield return new object[] { new Guid("00000000-0000-0000-0000-000000000000"), false }; yield return new object[] { new Guid("00000000-0000-0000-0000-000000000001"), true }; yield return new object[] { new Guid("e2ce1f3b-17e5-412e-923b-6b4e268f31aa"), true }; } [Theory] [MemberData(nameof(NotEmpty_Should_CollectError_Data))] public void NotEmpty_Should_CollectError(Guid memberValue, bool expectedIsValid) { Tester.TestSingleRule( memberValue, m => m.NotEmpty(), expectedIsValid, MessageKey.GuidType.NotEmpty); } [Theory] [MemberData(nameof(NotEmpty_Should_CollectError_Data))] public void NotEmpty_Should_CollectError_FromNullable(Guid memberValue, bool expectedIsValid) { Tester.TestSingleRule( memberValue, m => m.NotEmpty(), expectedIsValid, MessageKey.GuidType.NotEmpty); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/ByteRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class ByteRulesTests { private static readonly Func Convert = i => (byte)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(byte model, byte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(byte model, byte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(byte model, byte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(byte model, byte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(byte model, byte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(byte model, byte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(byte model, byte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(byte model, byte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(byte min, byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(byte min, byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, byte.MinValue, byte.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(byte min, byte max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(byte min, byte max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(byte.MinValue, byte.MaxValue, 1)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(byte min, byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(byte min, byte model, byte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, byte.MinValue, byte.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(byte min, byte max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(byte min, byte max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Unsigned_Limits(byte.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(byte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(byte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(byte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(byte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(byte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(byte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/CharNumbersRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class CharNumbersRulesTests { private static readonly Func Convert = i => (char)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(char model, char value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(char model, char value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(char model, char value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(char model, char value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(char model, char min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(char model, char min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(char model, char min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(char model, char min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(char min, char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(char min, char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, char.MinValue, char.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(char min, char max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(char min, char max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(char.MinValue, char.MaxValue, 1)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(char min, char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(char min, char model, char max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, char.MinValue, char.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(char min, char max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(char min, char max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Unsigned_Limits(char.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(char model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(char model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(char model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(char model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(char model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(char model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/DecimalRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class DecimalRulesTests { private static readonly Func Convert = i => i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(decimal model, decimal value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(decimal model, decimal value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(decimal model, decimal value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(decimal model, decimal value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(decimal model, decimal min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(decimal model, decimal min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Signed(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(decimal model, decimal min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(decimal model, decimal min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Signed(Convert), NumbersTestData.LessThanOrEqualTo_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(decimal min, decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(decimal min, decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, decimal.MinValue, decimal.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(decimal min, decimal max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(decimal min, decimal max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Signed(Convert), NumbersTestData.BetweenOrEqualTo_Limits(decimal.MinValue, decimal.MaxValue, 0)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(decimal min, decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(decimal min, decimal model, decimal max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, decimal.MinValue, decimal.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(decimal min, decimal max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(decimal min, decimal max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(decimal.MinValue, decimal.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert)); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert)); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(decimal model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/DoubleRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class DoubleRulesTests { private static readonly Func Convert = i => i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(double.MinValue, double.MaxValue, 0), new[] { new object[] { 0.999999D, 0D, false } }, new[] { new object[] { 1.000001D, 0D, false } }, new[] { new object[] { 1.123456D, 1.123456D, true } }); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(double model, double value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001D)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(double model, double value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001D)); } public static IEnumerable EqualTo_WithTolerance_Should_CollectError_MemberData() { return RulesHelper.GetTestDataCombined( new[] { new object[] { 1.000100D, 1.000199D, 0.0000001D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 0.000001D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 0.00001D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 0.0001D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 0.001D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 0.01D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 0.1D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 1D, true } }); } [Theory] [MemberData(nameof(EqualTo_WithTolerance_Should_CollectError_MemberData))] public void EqualTo_WithTolerance_Should_CollectError(double model, double value, double tolerance, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, tolerance), expectedIsValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } [Theory] [MemberData(nameof(EqualTo_WithTolerance_Should_CollectError_MemberData))] public void EqualTo_WithTolerance_Should_CollectError_FromNullable(double model, double value, double tolerance, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, tolerance), expectedIsValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(double.MinValue, double.MaxValue, 0), new[] { new object[] { 0.999999D, 0D, true } }, new[] { new object[] { 1.000001D, 0D, true } }, new[] { new object[] { 1.123456D, 1.123456D, false } }); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(double model, double value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001D)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(double model, double value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001D)); } public static IEnumerable NotEqualTo_WithTolerance_Should_CollectError_MemberData() { return RulesHelper.GetTestDataCombined( new[] { new object[] { 1.000100D, 1.000199D, 0.0000001D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 0.000001D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 0.00001D, true } }, new[] { new object[] { 1.000100D, 1.000199D, 0.0001D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 0.001D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 0.01D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 0.1D, false } }, new[] { new object[] { 1.000100D, 1.000199D, 1D, false } }); } [Theory] [MemberData(nameof(NotEqualTo_WithTolerance_Should_CollectError_MemberData))] public void NotEqualTo_WithTolerance_Should_CollectError(double model, double value, double tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, tolerance), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } [Theory] [MemberData(nameof(NotEqualTo_WithTolerance_Should_CollectError_MemberData))] public void NotEqualTo_WithTolerance_Should_CollectError_FromNullable(double model, double value, double tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, tolerance), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(double.MinValue, double.MaxValue, 0), new[] { new object[] { 0.999999D, 1D, false } }, new[] { new object[] { 1.000001D, 1D, true } }, new[] { new object[] { 0.999999D, 0.999999D, false } }, new[] { new object[] { 1D, 1.000001D, false } }, new[] { new object[] { 1.000001D, 1.000001D, false } }); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(double model, double min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(double model, double min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(double.MinValue, double.MaxValue, 0), new[] { new object[] { 0.999999D, 1D, true } }, new[] { new object[] { 1.000001D, 1D, false } }, new[] { new object[] { 0.999999D, 0.999999D, false } }, new[] { new object[] { 1D, 1.000001D, true } }, new[] { new object[] { 1.000001D, 1.000001D, false } }); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(double model, double max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(double model, double max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(double.MinValue, double.MaxValue, 0), new[] { new object[] { 0.999999D, 1, 1.000001D, true } }, new[] { new object[] { 0.999999D, 0.999999D, 1.000001D, false } }, new[] { new object[] { 0.999999D, 1.000001D, 1.000001D, false } }); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(double min, double model, double max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(double min, double model, double max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, double.MinValue, double.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(double min, double max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(double min, double max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(double.MinValue, double.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", 0.0000001D)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", 0.0000001D)); } public static IEnumerable NonZero_WithTolerance_Should_CollectError_MemberData() { return RulesHelper.GetTestDataCombined( new[] { new object[] { 0.000100D, 0.0000001D, true } }, new[] { new object[] { 0.000100D, 0.000001D, true } }, new[] { new object[] { 0.000100D, 0.00001D, true } }, new[] { new object[] { 0.000100D, 0.0001D, true } }, new[] { new object[] { 0.000100D, 0.001D, false } }, new[] { new object[] { 0.000100D, 0.01D, false } }, new[] { new object[] { 0.000100D, 0.1D, false } }, new[] { new object[] { 0.000100D, 1D, false } }); } [Theory] [MemberData(nameof(NonZero_WithTolerance_Should_CollectError_MemberData))] public void NonZero_WithTolerance_Should_CollectError(double model, double tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(tolerance), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", tolerance)); } [Theory] [MemberData(nameof(NonZero_WithTolerance_Should_CollectError_MemberData))] public void NonZero_WithTolerance_Should_CollectError_FromNullable(double model, double tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(tolerance), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", tolerance)); } public static IEnumerable NonNan_Should_CollectError_MemberData() { yield return new object[] { 123.123D, true }; yield return new object[] { double.NaN, false }; } [Theory] [MemberData(nameof(NonNan_Should_CollectError_MemberData))] public void NonNan_Should_CollectError(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNaN(), shouldBeValid, MessageKey.Numbers.NonNaN); } [Theory] [MemberData(nameof(NonNan_Should_CollectError_MemberData))] public void NonNan_Should_CollectError_FromNullable(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNaN(), shouldBeValid, MessageKey.Numbers.NonNaN); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert), new[] { new object[] { 0.000001D, true } }, new[] { new object[] { -0.000001D, false } }); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert), new[] { new object[] { 0.000001D, false } }, new[] { new object[] { -0.000001D, true } }); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert), new[] { new object[] { 0.000001D, false } }, new[] { new object[] { -0.000001D, true } }); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert), new[] { new object[] { 0.000001D, true } }, new[] { new object[] { -0.000001D, false } }); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(double model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/FloatRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class FloatRulesTests { private static readonly Func Convert = i => i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(float.MinValue, float.MaxValue, 0), new[] { new object[] { 0.999999F, 0F, false } }, new[] { new object[] { 1.000001F, 0F, false } }, new[] { new object[] { 1.123456F, 1.123456F, true } }); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(float model, float value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001F)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(float model, float value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001F)); } public static IEnumerable EqualTo_WithTolerance_Should_CollectError_MemberData() { return RulesHelper.GetTestDataCombined( new[] { new object[] { 1.000100F, 1.000199F, 0.0000001F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 0.000001F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 0.00001F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 0.0001F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 0.001F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 0.01F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 0.1F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 1F, true } }); } [Theory] [MemberData(nameof(EqualTo_WithTolerance_Should_CollectError_MemberData))] public void EqualTo_WithTolerance_Should_CollectError(float model, float value, float tolerance, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, tolerance), expectedIsValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } [Theory] [MemberData(nameof(EqualTo_WithTolerance_Should_CollectError_MemberData))] public void EqualTo_WithTolerance_Should_CollectError_FromNullable(float model, float value, float tolerance, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, tolerance), expectedIsValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(float.MinValue, float.MaxValue, 0), new[] { new object[] { 0.999999F, 0F, true } }, new[] { new object[] { 1.000001F, 0F, true } }, new[] { new object[] { 1.123456F, 1.123456F, false } }); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(float model, float value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001F)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(float model, float value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", 0.0000001F)); } public static IEnumerable NotEqualTo_WithTolerance_Should_CollectError_MemberData() { return RulesHelper.GetTestDataCombined( new[] { new object[] { 1.000100F, 1.000199F, 0.0000001F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 0.000001F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 0.00001F, true } }, new[] { new object[] { 1.000100F, 1.000199F, 0.0001F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 0.001F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 0.01F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 0.1F, false } }, new[] { new object[] { 1.000100F, 1.000199F, 1F, false } }); } [Theory] [MemberData(nameof(NotEqualTo_WithTolerance_Should_CollectError_MemberData))] public void NotEqualTo_WithTolerance_Should_CollectError(float model, float value, float tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, tolerance), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } [Theory] [MemberData(nameof(NotEqualTo_WithTolerance_Should_CollectError_MemberData))] public void NotEqualTo_WithTolerance_Should_CollectError_FromNullable(float model, float value, float tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, tolerance), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value), Arg.Number("tolerance", tolerance)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(float.MinValue, float.MaxValue, 0), new[] { new object[] { 0.999999F, 1F, false } }, new[] { new object[] { 1.000001F, 1F, true } }, new[] { new object[] { 0.999999F, 0.999999F, false } }, new[] { new object[] { 1F, 1.000001F, false } }, new[] { new object[] { 1.000001F, 1.000001F, false } }); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(float model, float min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(float model, float min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(float.MinValue, float.MaxValue, 0), new[] { new object[] { 0.999999F, 1F, true } }, new[] { new object[] { 1.000001F, 1F, false } }, new[] { new object[] { 0.999999F, 0.999999F, false } }, new[] { new object[] { 1F, 1.000001F, true } }, new[] { new object[] { 1.000001F, 1.000001F, false } }); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(float model, float max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(float model, float max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(float.MinValue, float.MaxValue, 0), new[] { new object[] { 0.999999F, 1, 1.000001F, true } }, new[] { new object[] { 0.999999F, 0.999999F, 1.000001F, false } }, new[] { new object[] { 0.999999F, 1.000001F, 1.000001F, false } }); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(float min, float model, float max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(float min, float model, float max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, float.MinValue, float.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(float min, float max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(float min, float max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(float.MinValue, float.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", 0.0000001F)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", 0.0000001F)); } public static IEnumerable NonZero_WithTolerance_Should_CollectError_MemberData() { return RulesHelper.GetTestDataCombined( new[] { new object[] { 0.000100F, 0.0000001F, true } }, new[] { new object[] { 0.000100F, 0.000001F, true } }, new[] { new object[] { 0.000100F, 0.00001F, true } }, new[] { new object[] { 0.000100F, 0.0001F, true } }, new[] { new object[] { 0.000100F, 0.001F, false } }, new[] { new object[] { 0.000100F, 0.01F, false } }, new[] { new object[] { 0.000100F, 0.1F, false } }, new[] { new object[] { 0.000100F, 1F, false } }); } [Theory] [MemberData(nameof(NonZero_WithTolerance_Should_CollectError_MemberData))] public void NonZero_WithTolerance_Should_CollectError(float model, float tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(tolerance), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", tolerance)); } [Theory] [MemberData(nameof(NonZero_WithTolerance_Should_CollectError_MemberData))] public void NonZero_WithTolerance_Should_CollectError_FromNullable(float model, float tolerance, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(tolerance), shouldBeValid, MessageKey.Numbers.NonZero, Arg.Number("tolerance", tolerance)); } public static IEnumerable NonNan_Should_CollectError_MemberData() { yield return new object[] { 123.123F, true }; yield return new object[] { float.NaN, false }; } [Theory] [MemberData(nameof(NonNan_Should_CollectError_MemberData))] public void NonNan_Should_CollectError(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNaN(), shouldBeValid, MessageKey.Numbers.NonNaN); } [Theory] [MemberData(nameof(NonNan_Should_CollectError_MemberData))] public void NonNan_Should_CollectError_FromNullable(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNaN(), shouldBeValid, MessageKey.Numbers.NonNaN); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert), new[] { new object[] { 0.000001F, true } }, new[] { new object[] { -0.000001F, false } }); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert), new[] { new object[] { 0.000001F, false } }, new[] { new object[] { -0.000001F, true } }); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert), new[] { new object[] { 0.000001F, false } }, new[] { new object[] { -0.000001F, true } }); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert), new[] { new object[] { 0.000001F, true } }, new[] { new object[] { -0.000001F, false } }); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(float model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/IntRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class IntRulesTests { private static readonly Func Convert = i => i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(int model, int value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(int model, int value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(int model, int value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(int model, int value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(int model, int min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(int model, int min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Signed(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(int model, int min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(int model, int min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Signed(Convert), NumbersTestData.LessThanOrEqualTo_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(int min, int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(int min, int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, int.MinValue, int.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(int min, int max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Signed(Convert), NumbersTestData.BetweenOrEqualTo_Limits(int.MinValue, int.MaxValue, 0)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(int min, int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(int min, int model, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, int.MinValue, int.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(int min, int max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(int min, int max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(int.MinValue, int.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert)); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert)); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(int model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/LongRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class LongRulesTests { private static readonly Func Convert = i => i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(long model, long value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(long model, long value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(long model, long value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(long model, long value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(long model, long min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(long model, long min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Signed(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(long model, long min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(long model, long min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Signed(Convert), NumbersTestData.LessThanOrEqualTo_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(long min, long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(long min, long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, long.MinValue, long.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(long min, long max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(long min, long max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Signed(Convert), NumbersTestData.BetweenOrEqualTo_Limits(long.MinValue, long.MaxValue, 0)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(long min, long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(long min, long model, long max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, long.MinValue, long.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(long min, long max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(long min, long max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(long.MinValue, long.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert)); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert)); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(long model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/NumbersTestData.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; public static class NumbersTestData { public static IEnumerable EqualTo_Unsigned(Func convert) { yield return new object[] { convert(0), convert(3), false }; yield return new object[] { convert(2), convert(5), false }; yield return new object[] { convert(1), convert(1), true }; } public static IEnumerable EqualTo_Signed(Func convert) { yield return new object[] { convert(0), convert(-1), false }; yield return new object[] { convert(-2), convert(-5), false }; yield return new object[] { convert(-1), convert(-1), true }; yield return new object[] { convert(-2), convert(2), false }; } public static IEnumerable EqualTo_Limits(T min, T max, T neutral) { yield return new object[] { max, max, true }; yield return new object[] { min, max, false }; yield return new object[] { min, min, true }; yield return new object[] { min, neutral, false }; yield return new object[] { max, neutral, false }; } public static IEnumerable NotEqualTo_Unsigned(Func convert) { yield return new object[] { convert(0), convert(3), true }; yield return new object[] { convert(2), convert(5), true }; yield return new object[] { convert(1), convert(1), false }; } public static IEnumerable NotEqualTo_Signed(Func convert) { yield return new object[] { convert(0), convert(-1), true }; yield return new object[] { convert(-2), convert(-5), true }; yield return new object[] { convert(-1), convert(-1), false }; yield return new object[] { convert(-2), convert(2), true }; } public static IEnumerable NotEqualTo_Limits(T min, T max, T neutral) { yield return new object[] { max, max, false }; yield return new object[] { min, max, true }; yield return new object[] { min, min, false }; yield return new object[] { min, neutral, true }; yield return new object[] { max, neutral, true }; } public static IEnumerable GreaterThan_Unsigned(Func convert) { yield return new object[] { convert(0), convert(3), false }; yield return new object[] { convert(2), convert(1), true }; yield return new object[] { convert(1), convert(1), false }; yield return new object[] { convert(1), convert(0), true }; } public static IEnumerable GreaterThan_Signed(Func convert) { yield return new object[] { convert(0), convert(-1), true }; yield return new object[] { convert(-2), convert(-1), false }; yield return new object[] { convert(-1), convert(-1), false }; yield return new object[] { convert(2), convert(-2), true }; } public static IEnumerable GreaterThan_Limits(T min, T max, T neutral) { yield return new object[] { max, max, false }; yield return new object[] { min, max, false }; yield return new object[] { max, min, true }; yield return new object[] { min, min, false }; yield return new object[] { min, neutral, false }; yield return new object[] { max, neutral, true }; } public static IEnumerable GreaterThanOrEqualTo_Unsigned(Func convert) { yield return new object[] { convert(0), convert(3), false }; yield return new object[] { convert(2), convert(1), true }; yield return new object[] { convert(1), convert(1), true }; yield return new object[] { convert(1), convert(0), true }; } public static IEnumerable GreaterThanOrEqualTo_Signed(Func convert) { yield return new object[] { convert(0), convert(-1), true }; yield return new object[] { convert(-2), convert(-1), false }; yield return new object[] { convert(-1), convert(-1), true }; yield return new object[] { convert(2), convert(-2), true }; } public static IEnumerable GreaterThanOrEqualTo_Limits(T min, T max, T neutral) { yield return new object[] { max, max, true }; yield return new object[] { min, max, false }; yield return new object[] { max, min, true }; yield return new object[] { min, min, true }; yield return new object[] { min, neutral, false }; yield return new object[] { max, neutral, true }; } public static IEnumerable LessThan_Unsigned(Func convert) { yield return new object[] { convert(0), convert(3), true }; yield return new object[] { convert(2), convert(1), false }; yield return new object[] { convert(1), convert(1), false }; yield return new object[] { convert(1), convert(0), false }; } public static IEnumerable LessThan_Signed(Func convert) { yield return new object[] { convert(0), convert(-1), false }; yield return new object[] { convert(-2), convert(-1), true }; yield return new object[] { convert(-1), convert(-1), false }; yield return new object[] { convert(2), convert(-2), false }; } public static IEnumerable LessThan_Limits(T min, T max, T neutral) { yield return new object[] { max, max, false }; yield return new object[] { min, max, true }; yield return new object[] { max, min, false }; yield return new object[] { min, min, false }; yield return new object[] { min, neutral, true }; yield return new object[] { max, neutral, false }; } public static IEnumerable LessThanOrEqualTo_Unsigned(Func convert) { yield return new object[] { convert(0), convert(3), true }; yield return new object[] { convert(2), convert(1), false }; yield return new object[] { convert(1), convert(1), true }; yield return new object[] { convert(1), convert(0), false }; } public static IEnumerable LessThanOrEqualTo_Signed(Func convert) { yield return new object[] { convert(0), convert(-1), false }; yield return new object[] { convert(-2), convert(-1), true }; yield return new object[] { convert(-1), convert(-1), true }; yield return new object[] { convert(2), convert(-2), false }; } public static IEnumerable LessThanOrEqualTo_Limits(T min, T max, T neutral) { yield return new object[] { max, max, true }; yield return new object[] { min, max, true }; yield return new object[] { max, min, false }; yield return new object[] { min, min, true }; yield return new object[] { min, neutral, true }; yield return new object[] { max, neutral, false }; } public static IEnumerable Between_Unsigned(Func convert) { yield return new object[] { convert(1), convert(1), convert(3), false }; yield return new object[] { convert(1), convert(2), convert(3), true }; yield return new object[] { convert(1), convert(3), convert(3), false }; yield return new object[] { convert(1), convert(0), convert(3), false }; yield return new object[] { convert(1), convert(4), convert(3), false }; yield return new object[] { convert(3), convert(3), convert(3), false }; yield return new object[] { convert(3), convert(4), convert(3), false }; } public static IEnumerable Between_Signed(Func convert) { yield return new object[] { convert(-1), convert(0), convert(1), true }; yield return new object[] { convert(-1), convert(-1), convert(1), false }; yield return new object[] { convert(-1), convert(1), convert(1), false }; yield return new object[] { convert(-1), convert(2), convert(1), false }; yield return new object[] { convert(-1), convert(-2), convert(1), false }; yield return new object[] { convert(1), convert(1), convert(1), false }; yield return new object[] { convert(-3), convert(-2), convert(-3), false }; yield return new object[] { convert(-3), convert(-2), convert(-1), true }; } public static IEnumerable Between_Limits(T min, T max, T neutral) { yield return new object[] { max, neutral, max, false }; yield return new object[] { min, neutral, max, true }; yield return new object[] { min, max, max, false }; yield return new object[] { min, min, max, false }; yield return new object[] { min, min, min, false }; yield return new object[] { max, max, max, false }; } public static IEnumerable Between_InvalidRange(Func convert, T min, T max) { yield return new object[] { convert(2), convert(1) }; yield return new object[] { max, min }; } public static IEnumerable BetweenOrEqualTo_Signed(Func convert) { yield return new object[] { convert(-1), convert(0), convert(1), true }; yield return new object[] { convert(-1), convert(-1), convert(1), true }; yield return new object[] { convert(-1), convert(1), convert(1), true }; yield return new object[] { convert(-1), convert(2), convert(1), false }; yield return new object[] { convert(-1), convert(-2), convert(1), false }; yield return new object[] { convert(1), convert(1), convert(1), true }; yield return new object[] { convert(-3), convert(-2), convert(-3), false }; yield return new object[] { convert(-3), convert(-2), convert(-1), true }; } public static IEnumerable BetweenOrEqualTo_Unsigned(Func convert) { yield return new object[] { convert(1), convert(1), convert(3), true }; yield return new object[] { convert(1), convert(2), convert(3), true }; yield return new object[] { convert(1), convert(3), convert(3), true }; yield return new object[] { convert(1), convert(0), convert(3), false }; yield return new object[] { convert(1), convert(4), convert(3), false }; yield return new object[] { convert(3), convert(3), convert(3), true }; yield return new object[] { convert(3), convert(4), convert(3), false }; } public static IEnumerable BetweenOrEqualTo_Limits(T min, T max, T neutral) { yield return new object[] { max, neutral, max, false }; yield return new object[] { min, neutral, max, true }; yield return new object[] { min, max, max, true }; yield return new object[] { min, min, max, true }; yield return new object[] { min, min, min, true }; yield return new object[] { max, max, max, true }; } public static IEnumerable NonZero_Signed(Func convert) { yield return new object[] { convert(-1), true }; yield return new object[] { convert(-10), true }; } public static IEnumerable NonZero_Unsigned(Func convert) { yield return new object[] { convert(0), false }; yield return new object[] { convert(1), true }; yield return new object[] { convert(5), true }; } public static IEnumerable NonZero_Signed_Limits(T min, T max) { yield return new object[] { min, true }; yield return new object[] { max, true }; } public static IEnumerable NonZero_Unsigned_Limits(T max) { yield return new object[] { max, true }; } public static IEnumerable Positive_Signed(Func convert) { yield return new object[] { convert(-1), false }; yield return new object[] { convert(-10), false }; } public static IEnumerable Positive_Unsigned(Func convert) { yield return new object[] { convert(0), false }; yield return new object[] { convert(1), true }; yield return new object[] { convert(10), true }; } public static IEnumerable NonPositive_Signed(Func convert) { yield return new object[] { convert(-1), true }; yield return new object[] { convert(-10), true }; } public static IEnumerable NonPositive_Unsigned(Func convert) { yield return new object[] { convert(0), true }; yield return new object[] { convert(1), false }; yield return new object[] { convert(10), false }; } public static IEnumerable Negative(Func convert) { yield return new object[] { convert(0), false }; yield return new object[] { convert(1), false }; yield return new object[] { convert(10), false }; yield return new object[] { convert(-1), true }; yield return new object[] { convert(-10), true }; } public static IEnumerable NonNegative(Func convert) { yield return new object[] { convert(0), true }; yield return new object[] { convert(1), true }; yield return new object[] { convert(10), true }; yield return new object[] { convert(-1), false }; yield return new object[] { convert(-10), false }; } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/SByteRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class SByteRulesTests { private static readonly Func Convert = i => (sbyte)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(sbyte model, sbyte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(sbyte model, sbyte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(sbyte model, sbyte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(sbyte model, sbyte value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(sbyte model, sbyte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(sbyte model, sbyte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Signed(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(sbyte model, sbyte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(sbyte model, sbyte min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Signed(Convert), NumbersTestData.LessThanOrEqualTo_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(sbyte min, sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(sbyte min, sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, sbyte.MinValue, sbyte.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(sbyte min, sbyte max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(sbyte min, sbyte max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Signed(Convert), NumbersTestData.BetweenOrEqualTo_Limits(sbyte.MinValue, sbyte.MaxValue, 0)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(sbyte min, sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(sbyte min, sbyte model, sbyte max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, sbyte.MinValue, sbyte.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(sbyte min, sbyte max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(sbyte min, sbyte max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(sbyte.MinValue, sbyte.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert)); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert)); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(sbyte model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/ShortRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class ShortRulesTests { private static readonly Func Convert = i => (short)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(short model, short value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(short model, short value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(short model, short value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(short model, short value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(short model, short min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(short model, short min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Signed(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(short model, short min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(short model, short min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Signed(Convert), NumbersTestData.LessThanOrEqualTo_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(short min, short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(short min, short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, short.MinValue, short.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(short min, short max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(short min, short max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Signed(Convert), NumbersTestData.BetweenOrEqualTo_Limits(short.MinValue, short.MaxValue, 0)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(short min, short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(short min, short model, short max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, short.MinValue, short.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(short min, short max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(short min, short max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(short.MinValue, short.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert)); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.Numbers.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert)); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(short model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.Numbers.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/UIntRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class UIntRulesTests { private static readonly Func Convert = i => (uint)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(uint model, uint value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(uint model, uint value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(uint model, uint value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(uint model, uint value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(uint model, uint min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(uint model, uint min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(uint model, uint min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(uint model, uint min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(uint min, uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(uint min, uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, uint.MinValue, uint.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(uint min, uint max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(uint min, uint max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(uint.MinValue, uint.MaxValue, 1U)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(uint min, uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(uint min, uint model, uint max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, uint.MinValue, uint.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(uint min, uint max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(uint min, uint max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Unsigned_Limits(uint.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(uint model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(uint model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(uint model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(uint model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(uint model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(uint model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/ULongRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class ULongRulesTests { private static readonly Func Convert = i => (ulong)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(ulong model, ulong value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(ulong model, ulong value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(ulong model, ulong value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(ulong model, ulong value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(ulong model, ulong min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(ulong model, ulong min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(ulong model, ulong min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(ulong model, ulong min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(ulong min, ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(ulong min, ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, ulong.MinValue, ulong.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(ulong min, ulong max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(ulong min, ulong max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(ulong.MinValue, ulong.MaxValue, 1UL)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(ulong min, ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(ulong min, ulong model, ulong max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, ulong.MinValue, ulong.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(ulong min, ulong max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(ulong min, ulong max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Unsigned_Limits(ulong.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(ulong model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(ulong model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(ulong model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(ulong model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(ulong model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(ulong model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Numbers/UShortRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Numbers { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class UShortRulesTests { private static readonly Func Convert = i => (ushort)i; public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(ushort model, ushort value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(ushort model, ushort value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Numbers.EqualTo, Arg.Number("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(ushort model, ushort value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(ushort model, ushort value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Numbers.NotEqualTo, Arg.Number("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(ushort model, ushort min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(ushort model, ushort min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.Numbers.GreaterThan, Arg.Number("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(ushort model, ushort min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(ushort model, ushort min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.Numbers.GreaterThanOrEqualTo, Arg.Number("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.Numbers.LessThan, Arg.Number("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.Numbers.LessThanOrEqualTo, Arg.Number("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(ushort min, ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(ushort min, ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Numbers.Between, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, ushort.MinValue, ushort.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(ushort min, ushort max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(ushort min, ushort max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(ushort.MinValue, ushort.MaxValue, 1)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(ushort min, ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(ushort min, ushort model, ushort max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Numbers.BetweenOrEqualTo, Arg.Number("min", min), Arg.Number("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, ushort.MinValue, ushort.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(ushort min, ushort max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(ushort min, ushort max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Unsigned_Limits(ushort.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(ushort model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(ushort model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.Numbers.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(ushort model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(ushort model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.Numbers.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(ushort model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(ushort model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.Numbers.NonPositive); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/RulesHelper.cs ================================================ namespace Validot.Tests.Unit.Rules { using System.Collections.Generic; using System.Linq; public static class RulesHelper { public static IEnumerable GetTestDataCombined(params IEnumerable[] sets) { return sets.SelectMany(s => s); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Text/CharRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Text { using Validot.Testing; using Validot.Translations; using Xunit; public class CharRulesTests { [Theory] [InlineData('a', 'a', true)] [InlineData('A', 'a', true)] [InlineData('a', 'A', true)] [InlineData('A', 'A', true)] [InlineData('A', 'b', false)] [InlineData('a', 'B', false)] [InlineData('a', 'b', false)] [InlineData('A', 'B', false)] [InlineData('Ż', 'Ż', true)] [InlineData('ć', 'Ć', true)] [InlineData('Ą', 'ó', false)] public void EqualToIgnoreCase_Should_CollectError(char modek, char value, bool expectedIsValid) { Tester.TestSingleRule( modek, m => m.EqualToIgnoreCase(value), expectedIsValid, MessageKey.CharType.EqualToIgnoreCase, Arg.Text("value", value)); } [Theory] [InlineData('a', 'a', false)] [InlineData('A', 'a', false)] [InlineData('a', 'A', false)] [InlineData('A', 'A', false)] [InlineData('A', 'b', true)] [InlineData('a', 'B', true)] [InlineData('a', 'b', true)] [InlineData('A', 'B', true)] [InlineData('Ż', 'Ż', false)] [InlineData('ć', 'Ć', false)] [InlineData('Ą', 'ó', true)] public void NotEqualToIgnoreCase_Should_CollectError(char model, char argValue, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.NotEqualToIgnoreCase(argValue), expectedIsValid, MessageKey.CharType.NotEqualToIgnoreCase, Arg.Text("value", argValue)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Text/EmailRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Text { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Translations; using Xunit; public class EmailRulesTests { [Theory] [InlineData(-1)] [InlineData(20)] [InlineData(100)] public void Email_Should_ThrowException_When_EnumIsNotDefined(int mode) { Tester.TestExceptionOnInit(m => m.Email(mode: (EmailValidationMode)mode), typeof(ArgumentException)); } public static IEnumerable Email_UsingMode_ComplexRegex_Should_CollectError_Data() { var testCases = GetTestCases(); foreach (var testCase in testCases) { yield return new object[] { testCase.Key, testCase.Value.complexRegex }; } } [Theory] [MemberData(nameof(Email_UsingMode_ComplexRegex_Should_CollectError_Data))] public void Email_UsingMode_ComplexRegex_Should_CollectError(string model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.Email(mode: EmailValidationMode.ComplexRegex), expectedIsValid, MessageKey.Texts.Email); } [Theory] [MemberData(nameof(Email_UsingMode_ComplexRegex_Should_CollectError_Data))] public void Email_Should_CollectError_WithComplexRegexMode_ByDefault(string model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.Email(), expectedIsValid, MessageKey.Texts.Email); } public static IEnumerable Email_UsingMode_DataAnnotationsCompatible_Should_CollectError_Data() { var testCases = GetTestCases(); foreach (var testCase in testCases) { yield return new object[] { testCase.Key, testCase.Value.dataAnnotations }; } } [Theory] [MemberData(nameof(Email_UsingMode_DataAnnotationsCompatible_Should_CollectError_Data))] public void Email_UsingMode_DataAnnotationsCompatible_Should_CollectError(string model, bool expectedIsValid) { Tester.TestSingleRule( model, m => m.Email(mode: EmailValidationMode.DataAnnotationsCompatible), expectedIsValid, MessageKey.Texts.Email); } private static Dictionary GetTestCases() { var dictionary = new Dictionary() { ["prettyandsimple@example.com"] = (complexRegex: true, dataAnnotations: true), ["very.common@example.com"] = (complexRegex: true, dataAnnotations: true), ["disposable.style.email.with+symbol@example.com"] = (complexRegex: true, dataAnnotations: true), ["other.email-with-dash@example.com"] = (complexRegex: true, dataAnnotations: true), ["fully-qualified-domain@example.com."] = (complexRegex: false, dataAnnotations: true), ["user.name+tag+sorting@example.com"] = (complexRegex: true, dataAnnotations: true), ["x@example.com"] = (complexRegex: true, dataAnnotations: true), ["example-indeed@strange-example.com"] = (complexRegex: true, dataAnnotations: true), ["admin@mailserver1"] = (complexRegex: false, dataAnnotations: true), ["email@123.123.123.123"] = (complexRegex: true, dataAnnotations: true), ["email@[123.123.123.123]"] = (complexRegex: true, dataAnnotations: true), ["1234567890@example.com"] = (complexRegex: true, dataAnnotations: true), ["#!$%&'*+-/=?^_`{}|~@example.org"] = (complexRegex: false, dataAnnotations: true), [@"""()<>[]:,;@\\\""!#$%&'-/=?^_`{}| ~.a""@example.org"] = (complexRegex: true, dataAnnotations: false), ["Abc.example.com"] = (complexRegex: false, dataAnnotations: false), ["A@b@c@example.com"] = (complexRegex: false, dataAnnotations: false), [@"a""b(c)d,e:f;gi[j\k]l@example.com"] = (complexRegex: false, dataAnnotations: true), [@"just""not""right@example.com"] = (complexRegex: false, dataAnnotations: true), [@"this is""not\allowed@example.com"] = (complexRegex: false, dataAnnotations: true), [@"this\ still\""not\\allowed@example.com"] = (complexRegex: false, dataAnnotations: true), ["Duy"] = (complexRegex: false, dataAnnotations: false), [" email@example.com"] = (complexRegex: false, dataAnnotations: true), ["email@example.com "] = (complexRegex: false, dataAnnotations: true), [""] = (complexRegex: false, dataAnnotations: false), ["david.jones@proseware.com"] = (complexRegex: true, dataAnnotations: true), ["d.j@server1.proseware.com"] = (complexRegex: true, dataAnnotations: true), ["jones@ms1.proseware.com"] = (complexRegex: true, dataAnnotations: true), ["j.@server1.proseware.com"] = (complexRegex: false, dataAnnotations: true), ["j@proseware.com9"] = (complexRegex: true, dataAnnotations: true), ["js#internal@proseware.com"] = (complexRegex: true, dataAnnotations: true), ["j_9@[129.126.118.1]"] = (complexRegex: true, dataAnnotations: true), ["j..s@proseware.com"] = (complexRegex: false, dataAnnotations: true), ["js*@proseware.com"] = (complexRegex: false, dataAnnotations: true), ["js@proseware..com"] = (complexRegex: false, dataAnnotations: true), ["js@proseware.com9"] = (complexRegex: true, dataAnnotations: true), ["j.s@server1.proseware.com"] = (complexRegex: true, dataAnnotations: true), ["js@contoso.中国"] = (complexRegex: true, dataAnnotations: true), [@"""j""s""@proseware.com"] = (complexRegex: true, dataAnnotations: true), ["\u00A0@someDomain.com"] = (complexRegex: false, dataAnnotations: true), ["!#$%&'*+-/=?^_`|~@someDomain.com"] = (complexRegex: false, dataAnnotations: true), ["someName@some~domain.com"] = (complexRegex: false, dataAnnotations: true), ["someName@some_domain.com"] = (complexRegex: false, dataAnnotations: true), ["someName@1234.com"] = (complexRegex: true, dataAnnotations: true), ["someName@someDomain\uFFEF.com"] = (complexRegex: false, dataAnnotations: true), [" \r \t \n"] = (complexRegex: false, dataAnnotations: false), ["@someDomain.com"] = (complexRegex: false, dataAnnotations: false), ["@someDomain@abc.com"] = (complexRegex: false, dataAnnotations: false), ["someName"] = (complexRegex: false, dataAnnotations: false), ["someName@"] = (complexRegex: false, dataAnnotations: false), ["someName@a@b.com"] = (complexRegex: false, dataAnnotations: false), }; return dictionary; } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Text/StringRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Text { using System; using System.Collections.Generic; using System.Text.RegularExpressions; using Validot.Testing; using Validot.Translations; using Xunit; public class StringRulesTests { [Theory] [InlineData("abc", "abc", true)] [InlineData("!@#$%^&*()_[]{};':\",/.<>?~789456123", "!@#$%^&*()_[]{};':\",/.<>?~789456123", true)] [InlineData("ęóąśłżźć", "ęóąśłżźć", true)] [InlineData("ABC", "ABC", true)] [InlineData("", "", true)] [InlineData("", "#", false)] [InlineData("abc", "cba", false)] [InlineData("abc", "abcd", false)] [InlineData("abc", "ABC", false)] [InlineData("abc", " abc ", false)] [InlineData("ĘÓĄŚŁŻŹĆ", "EOASLZZC", false)] public void EqualTo_Should_CollectError(string model, string value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Texts.EqualTo, Arg.Text("value", value), Arg.Enum("stringComparison", StringComparison.Ordinal)); } [Theory] [InlineData("abc", "abc", false)] [InlineData("!@#$%^&*()_[]{};':\",/.<>?~789456123", "!@#$%^&*()_[]{};':\",/.<>?~789456123", false)] [InlineData("ęóąśłżźć", "ęóąśłżźć", false)] [InlineData("ABC", "ABC", false)] [InlineData("", "", false)] [InlineData("", "#", true)] [InlineData("abc", "cba", true)] [InlineData("abc", "abcd", true)] [InlineData("abc", "ABC", true)] [InlineData("abc", " abc ", true)] [InlineData("ĘÓĄŚŁŻŹĆ", "EOASLZZC", true)] public void NotEqualTo_Should_CollectError(string model, string value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Texts.NotEqualTo, Arg.Text("value", value), Arg.Enum("stringComparison", StringComparison.Ordinal)); } [Theory] [InlineData("abc", "abc", true)] [InlineData("!@#$%^&*()_[]{};':\",/.<>?~789456123", "!@#$%^&*()_[]{};':\",/.<>?~789456123", true)] [InlineData("ęóąśłżźć", "ęóąśłżźć", true)] [InlineData("ĘÓĄŚŁŻŹĆ", "ęóąśłżźć", true)] [InlineData("ABC", "ABC", true)] [InlineData("abc", "ABC", true)] [InlineData("abc 123 !@# ĘÓĄŚŁŻŹĆ DEF", "ABC 123 !@# ęóąśłżźć def", true)] [InlineData("", "", true)] [InlineData("", "#", false)] [InlineData("abc", "cba", false)] [InlineData("abc", "abcd", false)] [InlineData("abc", " abc ", false)] [InlineData("ĘÓĄŚŁŻŹĆ", "EOASLZZC", false)] public void EqualTo_Should_CollectError_When_ComparisonIgnoreCase(string model, string value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, StringComparison.OrdinalIgnoreCase), shouldBeValid, MessageKey.Texts.EqualTo, Arg.Text("value", value), Arg.Enum("stringComparison", StringComparison.OrdinalIgnoreCase)); } [Theory] [InlineData("abc", "abc", false)] [InlineData("!@#$%^&*()_[]{};':\",/.<>?~789456123", "!@#$%^&*()_[]{};':\",/.<>?~789456123", false)] [InlineData("ęóąśłżźć", "ęóąśłżźć", false)] [InlineData("ĘÓĄŚŁŻŹĆ", "ęóąśłżźć", false)] [InlineData("ABC", "ABC", false)] [InlineData("abc", "ABC", false)] [InlineData("abc 123 !@# ĘÓĄŚŁŻŹĆ DEF", "ABC 123 !@# ęóąśłżźć def", false)] [InlineData("", "", false)] [InlineData("", "#", true)] [InlineData("abc", "cba", true)] [InlineData("abc", "abcd", true)] [InlineData("abc", " abc ", true)] [InlineData("ĘÓĄŚŁŻŹĆ", "EOASLZZC", true)] public void NotEqualTo_Should_CollectError_When_ComparisonIgnoreCase(string model, string value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, StringComparison.OrdinalIgnoreCase), shouldBeValid, MessageKey.Texts.NotEqualTo, Arg.Text("value", value), Arg.Enum("stringComparison", StringComparison.OrdinalIgnoreCase)); } public static IEnumerable Contains_Should_CollectError_Data() { yield return new object[] { $"test{Environment.NewLine}abc", "ABC", StringComparison.Ordinal, false }; yield return new object[] { $"test{Environment.NewLine}abc", "ABC", StringComparison.OrdinalIgnoreCase, true }; yield return new object[] { $"test{Environment.NewLine}abc", $"{Environment.NewLine}abc", StringComparison.OrdinalIgnoreCase, true }; yield return new object[] { $"test{Environment.NewLine}abc", $"{Environment.NewLine}ABC", StringComparison.OrdinalIgnoreCase, true }; yield return new object[] { $"test{Environment.NewLine}abc", $"{Environment.NewLine}ABC", StringComparison.Ordinal, false }; } [Theory] [InlineData("ruletest", "TEST", StringComparison.Ordinal, false)] [InlineData("ruletest", "TEST", StringComparison.OrdinalIgnoreCase, true)] [InlineData("ruletest", "test123", StringComparison.Ordinal, false)] [InlineData("ruletest", "test123", StringComparison.OrdinalIgnoreCase, false)] [InlineData("ruletest", "rule123", StringComparison.Ordinal, false)] [InlineData("ruletest", "rule123", StringComparison.OrdinalIgnoreCase, false)] [InlineData("abc !@# DEF", "abc !", StringComparison.Ordinal, true)] [InlineData("abc !@# DEF", "abc !", StringComparison.OrdinalIgnoreCase, true)] [InlineData("abc !@# DEF", "!@#", StringComparison.Ordinal, true)] [InlineData("abc !@# DEF", "!@#", StringComparison.OrdinalIgnoreCase, true)] [InlineData("abc !@# DEF", "# def", StringComparison.Ordinal, false)] [InlineData("abc !@# DEF", "# DEF", StringComparison.OrdinalIgnoreCase, true)] [MemberData(nameof(Contains_Should_CollectError_Data))] public void Contains_Should_CollectError(string model, string argValue, StringComparison stringComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Contains(argValue, stringComparison), shouldBeValid, MessageKey.Texts.Contains, Arg.Text("value", argValue), Arg.Enum("stringComparison", stringComparison)); } public static IEnumerable NotContains_Should_CollectError_Data() { yield return new object[] { $"test{Environment.NewLine}abc", "ABC", StringComparison.Ordinal, true }; yield return new object[] { $"test{Environment.NewLine}abc", "ABC", StringComparison.OrdinalIgnoreCase, false }; yield return new object[] { $"test{Environment.NewLine}abc", $"{Environment.NewLine}abc", StringComparison.OrdinalIgnoreCase, false }; yield return new object[] { $"test{Environment.NewLine}abc", $"{Environment.NewLine}ABC", StringComparison.OrdinalIgnoreCase, false }; yield return new object[] { $"test{Environment.NewLine}abc", $"{Environment.NewLine}ABC", StringComparison.Ordinal, true }; } [Theory] [InlineData("ruletest", "TEST", StringComparison.Ordinal, true)] [InlineData("ruletest", "TEST", StringComparison.OrdinalIgnoreCase, false)] [InlineData("ruletest", "test123", StringComparison.Ordinal, true)] [InlineData("ruletest", "test123", StringComparison.OrdinalIgnoreCase, true)] [InlineData("ruletest", "rule123", StringComparison.Ordinal, true)] [InlineData("ruletest", "rule123", StringComparison.OrdinalIgnoreCase, true)] [InlineData("abc !@# DEF", "abc !", StringComparison.Ordinal, false)] [InlineData("abc !@# DEF", "abc !", StringComparison.OrdinalIgnoreCase, false)] [InlineData("abc !@# DEF", "!@#", StringComparison.Ordinal, false)] [InlineData("abc !@# DEF", "!@#", StringComparison.OrdinalIgnoreCase, false)] [InlineData("abc !@# DEF", "# def", StringComparison.Ordinal, true)] [InlineData("abc !@# DEF", "# DEF", StringComparison.OrdinalIgnoreCase, false)] [MemberData(nameof(NotContains_Should_CollectError_Data))] public void NotContains_Should_CollectError(string model, string argValue, StringComparison stringComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotContains(argValue, stringComparison), shouldBeValid, MessageKey.Texts.NotContains, Arg.Text("value", argValue), Arg.Enum("stringComparison", stringComparison)); } public static IEnumerable NotEmpty_Should_CollectError_NewLines_Data() { yield return new object[] { $"{Environment.NewLine}", true }; yield return new object[] { $"\t{Environment.NewLine}{Environment.NewLine}", true }; } [Theory] [InlineData("abc", true)] [InlineData(" ", true)] [InlineData("", false)] [MemberData(nameof(NotEmpty_Should_CollectError_NewLines_Data))] public void NotEmpty_Should_CollectError(string model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEmpty(), shouldBeValid, MessageKey.Texts.NotEmpty); } public static IEnumerable NotWhiteSpace_Should_CollectError_NewLines_Data() { yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}_", true }; yield return new object[] { $"\t{Environment.NewLine}\t\t_", true }; yield return new object[] { $"{Environment.NewLine}", false }; yield return new object[] { $"\t{Environment.NewLine}", false }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}", false }; yield return new object[] { $"\t{Environment.NewLine}\t{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}", false }; } [Theory] [InlineData("abc", true)] [InlineData("\t\t\t\t_\t\t\t", true)] [InlineData(" ", false)] [InlineData("\t", false)] [InlineData("", false)] [MemberData(nameof(NotWhiteSpace_Should_CollectError_NewLines_Data))] public void NotWhiteSpace_Should_CollectError(string model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotWhiteSpace(), shouldBeValid, MessageKey.Texts.NotWhiteSpace); } public static IEnumerable SingleLine_Should_CollectError_NewLines_Data() { yield return new object[] { $"abc{Environment.NewLine}", false }; yield return new object[] { $"{Environment.NewLine}", false }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}", false }; yield return new object[] { $"a{Environment.NewLine}b", false }; yield return new object[] { $"\t{Environment.NewLine}{Environment.NewLine}", false }; } [Theory] [InlineData("", true)] [InlineData("abc", true)] [MemberData(nameof(SingleLine_Should_CollectError_NewLines_Data))] public void SingleLine_Should_CollectError(string model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.SingleLine(), shouldBeValid, MessageKey.Texts.SingleLine); } public static IEnumerable ExactLength_Should_CollectError_NewLines_Data() { yield return new object[] { $"abc{Environment.NewLine}", 4, true }; yield return new object[] { $"{Environment.NewLine}", 1, true }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}", 2, true }; yield return new object[] { $"a{Environment.NewLine}b", 3, true }; yield return new object[] { $"{Environment.NewLine}", 0, false }; } [Theory] [InlineData("abc", 3, true)] [InlineData("ĘÓĄŚŁŻŹĆ", 8, true)] [InlineData("123545", 6, true)] [InlineData("ABC_CDE", 7, true)] [InlineData("", 0, true)] [InlineData("1234567890", 10, true)] [InlineData("abc ", 3, false)] [InlineData("Ę Ó Ą Ś Ł Ż Ź Ć ", 15, false)] [MemberData(nameof(ExactLength_Should_CollectError_NewLines_Data))] public void ExactLength_Should_CollectError(string model, int argValue, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.ExactLength(argValue), shouldBeValid, MessageKey.Texts.ExactLength, Arg.Number("length", argValue)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void ExactLength_Should_ThrowException_When_NegativeLength(int argValue) { Tester.TestExceptionOnInit(m => m.ExactLength(argValue), typeof(ArgumentOutOfRangeException)); } public static IEnumerable MaxLength_Should_CollectError_NewLines_Data() { yield return new object[] { $"abc{Environment.NewLine}", 5, true }; yield return new object[] { $"{Environment.NewLine}", 1, true }; yield return new object[] { $"{Environment.NewLine}", 0, false }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}", 1, false }; yield return new object[] { $"a{Environment.NewLine}b", 3, true }; yield return new object[] { $"a{Environment.NewLine}b", 2, false }; } [Theory] [InlineData("abc", 3, true)] [InlineData("abc", 2, false)] [InlineData("", 0, true)] [InlineData("", 1, true)] [InlineData("abc1234567890", int.MaxValue, true)] [InlineData("\t\t\t", 3, true)] [InlineData("\t\t\t_", 3, false)] [InlineData("X", 0, false)] [MemberData(nameof(MaxLength_Should_CollectError_NewLines_Data))] public void MaxLength_Should_CollectError(string model, int argValue, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.MaxLength(argValue), shouldBeValid, MessageKey.Texts.MaxLength, Arg.Number("max", argValue)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MaxLength_Should_ThrowException_When_NegativeLength(int argValue) { Tester.TestExceptionOnInit(m => m.MaxLength(argValue), typeof(ArgumentOutOfRangeException)); } public static IEnumerable MinLength_Should_CollectError_NewLines_Data() { yield return new object[] { $"abc{Environment.NewLine}", 5, false }; yield return new object[] { $"{Environment.NewLine}", 0, true }; yield return new object[] { $"{Environment.NewLine}", 1, true }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}", 3, false }; yield return new object[] { $"a{Environment.NewLine}b", 3, true }; yield return new object[] { $"a{Environment.NewLine}b", 4, false }; } [Theory] [InlineData("abc", 3, true)] [InlineData("abc", 2, true)] [InlineData("abc", 4, false)] [InlineData("", 0, true)] [InlineData("", 1, false)] [InlineData("abc1234567890", int.MaxValue, false)] [InlineData("\t\t\t", 3, true)] [InlineData("\t\t\t_", 3, true)] [MemberData(nameof(MinLength_Should_CollectError_NewLines_Data))] public void MinLength_Should_CollectError(string model, int argValue, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.MinLength(argValue), shouldBeValid, MessageKey.Texts.MinLength, Arg.Number("min", argValue)); } [Theory] [InlineData(-1)] [InlineData(int.MinValue)] public void MinLength_Should_ThrowException_When_NegativeLength(int argValue) { Tester.TestExceptionOnInit(m => m.MinLength(argValue), typeof(ArgumentOutOfRangeException)); } public static IEnumerable LengthBetween_Should_CollectError_NewLines_Data() { yield return new object[] { $"abc{Environment.NewLine}", 0, 3, false }; yield return new object[] { $"abc{Environment.NewLine}", 0, 4, true }; yield return new object[] { $"{Environment.NewLine}", 0, 1, true }; yield return new object[] { $"{Environment.NewLine}", 1, int.MaxValue, true }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}", 0, 2, true }; yield return new object[] { $"{Environment.NewLine}{Environment.NewLine}", 0, 1, false }; yield return new object[] { $"a{Environment.NewLine}b", 2, 3, true }; yield return new object[] { $"a{Environment.NewLine}b", 0, 2, false }; } [Theory] [InlineData("abc", 0, 3, true)] [InlineData("abc", 1, 3, true)] [InlineData("abc", 2, 3, true)] [InlineData("abc", 3, 3, true)] [InlineData("abc", 3, 4, true)] [InlineData("abc", 0, 2, false)] [InlineData("abc", 1, 1, false)] [InlineData("abc", 0, 1, false)] [InlineData("abc", 0, int.MaxValue, true)] [InlineData("abc", 4, 5, false)] [InlineData("abc", 4, int.MaxValue, false)] [MemberData(nameof(LengthBetween_Should_CollectError_NewLines_Data))] public void LengthBetween_Should_CollectError(string model, int min, int max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LengthBetween(min, max), shouldBeValid, MessageKey.Texts.LengthBetween, Arg.Number("min", min), Arg.Number("max", max)); } [Theory] [InlineData(-1, 1)] [InlineData(1, -1)] [InlineData(int.MinValue, 1)] [InlineData(int.MinValue, int.MaxValue)] [InlineData(int.MinValue, int.MinValue)] public void LengthBetween_Should_ThrowException_When_NegativeLength(int min, int max) { Tester.TestExceptionOnInit(m => m.LengthBetween(min, max), typeof(ArgumentOutOfRangeException)); } [Theory] [InlineData(int.MaxValue, 0)] [InlineData(1, 0)] [InlineData(1000, 100)] public void LengthBetween_Should_ThrowException_When_MinGreaterThanMax(int min, int max) { Tester.TestExceptionOnInit(m => m.LengthBetween(min, max), typeof(ArgumentException)); } public static IEnumerable Matches_String_Should_CollectError_Data() { var numericPattern = @"^(0|[1-9][0-9]*)$"; yield return new object[] { numericPattern, "123", true }; yield return new object[] { numericPattern, "123.123", false }; yield return new object[] { numericPattern, "abc", false }; yield return new object[] { numericPattern, "123abc", false }; var alphabeticPattern = @"^[a-zA-Z]+$"; yield return new object[] { alphabeticPattern, "ABCabc", true }; yield return new object[] { alphabeticPattern, "ABC abc", false }; yield return new object[] { alphabeticPattern, "ABC.abc", false }; yield return new object[] { alphabeticPattern, "123", false }; yield return new object[] { alphabeticPattern, "123abc", false }; var containsUppercasePattern = @"[A-Z]"; yield return new object[] { containsUppercasePattern, "ABC", true }; yield return new object[] { containsUppercasePattern, "ABCabc", true }; yield return new object[] { containsUppercasePattern, string.Empty, false }; yield return new object[] { containsUppercasePattern, "abc", false }; yield return new object[] { containsUppercasePattern, "abc A", true }; var guidPattern = @"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$"; yield return new object[] { guidPattern, "60e2f13f-685d-4165-8615-66ae0a9fcf8d", true }; yield return new object[] { guidPattern, "60e2f13f-685d-4165-8615-66ae0a9fcf8da", false }; yield return new object[] { guidPattern, "60e2f13f-685d-4165-8615-66ae0-a9fcf8d", false }; yield return new object[] { guidPattern, "60e2f13f685d4165861566ae0a9fcf8d", false }; yield return new object[] { guidPattern, "x0e2f13f-685d-4165-8615-66ae0a9fcf8d", false }; } [Theory] [MemberData(nameof(Matches_String_Should_CollectError_Data))] public void Matches_String_Should_CollectError(string pattern, string model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Matches(pattern), shouldBeValid, MessageKey.Texts.Matches, Arg.Text("pattern", pattern)); } public static IEnumerable Matches_Regex_Should_CollectError_Data() { var numericPattern = new Regex(@"^(0|[1-9][0-9]*)$", RegexOptions.CultureInvariant); yield return new object[] { numericPattern, "123", true }; yield return new object[] { numericPattern, "123.123", false }; yield return new object[] { numericPattern, "abc", false }; yield return new object[] { numericPattern, "123abc", false }; var alphabeticPattern = new Regex(@"^[a-zA-Z]+$", RegexOptions.CultureInvariant); yield return new object[] { alphabeticPattern, "ABCabc", true }; yield return new object[] { alphabeticPattern, "ABC abc", false }; yield return new object[] { alphabeticPattern, "ABC.abc", false }; yield return new object[] { alphabeticPattern, "123", false }; yield return new object[] { alphabeticPattern, "123abc", false }; var containsUppercasePattern = new Regex(@"[A-Z]", RegexOptions.CultureInvariant); yield return new object[] { containsUppercasePattern, "ABC", true }; yield return new object[] { containsUppercasePattern, "ABCabc", true }; yield return new object[] { containsUppercasePattern, string.Empty, false }; yield return new object[] { containsUppercasePattern, "abc", false }; yield return new object[] { containsUppercasePattern, "abc A", true }; var guidPattern = new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$", RegexOptions.CultureInvariant); yield return new object[] { guidPattern, "60e2f13f-685d-4165-8615-66ae0a9fcf8d", true }; yield return new object[] { guidPattern, "60e2f13f-685d-4165-8615-66ae0a9fcf8da", false }; yield return new object[] { guidPattern, "60e2f13f-685d-4165-8615-66ae0-a9fcf8d", false }; yield return new object[] { guidPattern, "60e2f13f685d4165861566ae0a9fcf8d", false }; yield return new object[] { guidPattern, "x0e2f13f-685d-4165-8615-66ae0a9fcf8d", false }; } [Theory] [MemberData(nameof(Matches_Regex_Should_CollectError_Data))] public void Matches_Regex_Should_CollectError(Regex pattern, string model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Matches(pattern), shouldBeValid, MessageKey.Texts.Matches, Arg.Text("pattern", pattern.ToString())); } public static IEnumerable StartsWith_NewLine_Should_CollectError_Data() { yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}", StringComparison.Ordinal, true }; yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}d", StringComparison.Ordinal, true }; yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}D", StringComparison.Ordinal, false }; yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}D", StringComparison.OrdinalIgnoreCase, true }; yield return new object[] { $"{Environment.NewLine}", $"{Environment.NewLine}", StringComparison.Ordinal, true }; yield return new object[] { $"{Environment.NewLine}", $"{Environment.NewLine}", StringComparison.OrdinalIgnoreCase, true }; } [Theory] [InlineData("abc", "ab", StringComparison.Ordinal, true)] [InlineData("abc", "aB", StringComparison.Ordinal, false)] [InlineData("abc", "aB", StringComparison.OrdinalIgnoreCase, true)] [InlineData("abc", "abc", StringComparison.Ordinal, true)] [InlineData("abc", "abC", StringComparison.Ordinal, false)] [InlineData("abc", "abC", StringComparison.OrdinalIgnoreCase, true)] [MemberData(nameof(StartsWith_NewLine_Should_CollectError_Data))] public void StartsWith_Should_CollectError(string model, string value, StringComparison stringComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.StartsWith(value, stringComparison), shouldBeValid, MessageKey.Texts.StartsWith, Arg.Text("value", value), Arg.Enum("stringComparison", stringComparison)); } public static IEnumerable EndsWith_NewLine_Should_CollectError() { yield return new object[] { $"abc{Environment.NewLine}d", $"bc{Environment.NewLine}d", StringComparison.Ordinal, true }; yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}d", StringComparison.Ordinal, true }; yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}D", StringComparison.Ordinal, false }; yield return new object[] { $"abc{Environment.NewLine}d", $"abc{Environment.NewLine}D", StringComparison.OrdinalIgnoreCase, true }; yield return new object[] { $"{Environment.NewLine}", $"{Environment.NewLine}", StringComparison.Ordinal, true }; yield return new object[] { $"{Environment.NewLine}", $"{Environment.NewLine}", StringComparison.OrdinalIgnoreCase, true }; } [Theory] [InlineData("abc", "bc", StringComparison.Ordinal, true)] [InlineData("abc", "Bc", StringComparison.Ordinal, false)] [InlineData("abc", "Bc", StringComparison.OrdinalIgnoreCase, true)] [InlineData("abc", "abc", StringComparison.Ordinal, true)] [InlineData("abc", "Abc", StringComparison.Ordinal, false)] [InlineData("abc", "Abc", StringComparison.OrdinalIgnoreCase, true)] [MemberData(nameof(EndsWith_NewLine_Should_CollectError))] public void EndsWith_Should_CollectError(string model, string value, StringComparison stringComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EndsWith(value, stringComparison), shouldBeValid, MessageKey.Texts.EndsWith, Arg.Text("value", value), Arg.Enum("stringComparison", stringComparison)); } [Fact] public void Contains_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.Contains(null), typeof(ArgumentNullException)); } [Fact] public void EndsWith_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.EndsWith(null), typeof(ArgumentNullException)); } [Fact] public void EqualTo_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.EqualTo(null), typeof(ArgumentNullException)); } [Fact] public void Matches_Regex_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.Matches((Regex)null), typeof(ArgumentNullException)); } [Fact] public void Matches_String_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.Matches((string)null), typeof(ArgumentNullException)); } [Fact] public void NotContains_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.NotContains(null), typeof(ArgumentNullException)); } [Fact] public void NotEqualTo_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.NotEqualTo(null), typeof(ArgumentNullException)); } [Fact] public void StartsWith_Should_ThrowException_When_NullValue() { Tester.TestExceptionOnInit(m => m.StartsWith(null), typeof(ArgumentNullException)); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/TimeSpanRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules { using System; using System.Collections.Generic; using Validot; using Validot.Testing; using Validot.Tests.Unit.Rules.Numbers; using Validot.Translations; using Xunit; public class TimeSpanRulesTests { private static readonly Func Convert = i => new TimeSpan(i); public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Signed(Convert), NumbersTestData.EqualTo_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(TimeSpan model, TimeSpan value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.TimeSpanType.EqualTo, Arg.Time("value", value)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(TimeSpan model, TimeSpan value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.TimeSpanType.EqualTo, Arg.Time("value", value)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Signed(Convert), NumbersTestData.NotEqualTo_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(TimeSpan model, TimeSpan value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.TimeSpanType.NotEqualTo, Arg.Time("value", value)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(TimeSpan model, TimeSpan value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.TimeSpanType.NotEqualTo, Arg.Time("value", value)); } public static IEnumerable GreaterThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Signed(Convert), NumbersTestData.GreaterThan_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError(TimeSpan model, TimeSpan min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.TimeSpanType.GreaterThan, Arg.Time("min", min)); } [Theory] [MemberData(nameof(GreaterThan_Should_CollectError_Data))] public void GreaterThan_Should_CollectError_FromNullable(TimeSpan model, TimeSpan min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThan(min), shouldBeValid, MessageKey.TimeSpanType.GreaterThan, Arg.Time("min", min)); } public static IEnumerable GreaterThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Signed(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError(TimeSpan model, TimeSpan min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.TimeSpanType.GreaterThanOrEqualTo, Arg.Time("min", min)); } [Theory] [MemberData(nameof(GreaterThanOrEqualTo_Should_CollectError_Data))] public void GreaterThanOrEqualTo_Should_CollectError_FromNullable(TimeSpan model, TimeSpan min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.GreaterThanOrEqualTo(min), shouldBeValid, MessageKey.TimeSpanType.GreaterThanOrEqualTo, Arg.Time("min", min)); } public static IEnumerable LessThan_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Signed(Convert), NumbersTestData.LessThan_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError(TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.TimeSpanType.LessThan, Arg.Time("max", max)); } [Theory] [MemberData(nameof(LessThan_Should_CollectError_Data))] public void LessThan_Should_CollectError_FromNullable(TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThan(max), shouldBeValid, MessageKey.TimeSpanType.LessThan, Arg.Time("max", max)); } public static IEnumerable LessThanOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Signed(Convert), NumbersTestData.LessThanOrEqualTo_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError(TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.TimeSpanType.LessThanOrEqualTo, Arg.Time("max", max)); } [Theory] [MemberData(nameof(LessThanOrEqualTo_Should_CollectError_Data))] public void LessThanOrEqualTo_Should_CollectError_FromNullable(TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.LessThanOrEqualTo(max), shouldBeValid, MessageKey.TimeSpanType.LessThanOrEqualTo, Arg.Time("max", max)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Signed(Convert), NumbersTestData.Between_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(TimeSpan min, TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.TimeSpanType.Between, Arg.Time("min", min), Arg.Time("max", max)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(TimeSpan min, TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.TimeSpanType.Between, Arg.Time("min", min), Arg.Time("max", max)); } public static IEnumerable Between_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, TimeSpan.MinValue, TimeSpan.MaxValue)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax(TimeSpan min, TimeSpan max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(Between_Should_ThrowException_When_MinLargerThanMax_Data))] public void Between_Should_ThrowException_When_MinLargerThanMax_FromNullable(TimeSpan min, TimeSpan max) { Tester.TestExceptionOnInit( s => s.Between(min, max), typeof(ArgumentException)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Signed(Convert), NumbersTestData.BetweenOrEqualTo_Limits(TimeSpan.MinValue, TimeSpan.MaxValue, TimeSpan.Zero)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(TimeSpan min, TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.TimeSpanType.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(TimeSpan min, TimeSpan model, TimeSpan max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.TimeSpanType.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max)); } public static IEnumerable BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_InvalidRange(Convert, TimeSpan.MinValue, TimeSpan.MaxValue)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax(TimeSpan min, TimeSpan max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_Data))] public void BetweenOrEqualTo_Should_ThrowException_When_MinLargerThanMax_FromNullable(TimeSpan min, TimeSpan max) { Tester.TestExceptionOnInit( s => s.BetweenOrEqualTo(min, max), typeof(ArgumentException)); } public static IEnumerable NonZero_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonZero_Unsigned(Convert), NumbersTestData.NonZero_Unsigned_Limits(TimeSpan.MaxValue), NumbersTestData.NonZero_Signed(Convert), NumbersTestData.NonZero_Signed_Limits(TimeSpan.MinValue, TimeSpan.MaxValue)); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.TimeSpanType.NonZero); } [Theory] [MemberData(nameof(NonZero_Should_CollectError_Data))] public void NonZero_Should_CollectError_FromNullable(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonZero(), shouldBeValid, MessageKey.TimeSpanType.NonZero); } public static IEnumerable Positive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Positive_Unsigned(Convert), NumbersTestData.Positive_Signed(Convert)); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.TimeSpanType.Positive); } [Theory] [MemberData(nameof(Positive_Should_CollectError_Data))] public void Positive_Should_CollectError_FromNullable(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Positive(), shouldBeValid, MessageKey.TimeSpanType.Positive); } public static IEnumerable NonPositive_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonPositive_Unsigned(Convert), NumbersTestData.NonPositive_Signed(Convert)); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.TimeSpanType.NonPositive); } [Theory] [MemberData(nameof(NonPositive_Should_CollectError_Data))] public void NonPositive_Should_CollectError_FromNullable(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonPositive(), shouldBeValid, MessageKey.TimeSpanType.NonPositive); } public static IEnumerable Negative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Negative(Convert)); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.TimeSpanType.Negative); } [Theory] [MemberData(nameof(Negative_Should_CollectError_Data))] public void Negative_Should_CollectError_FromNullable(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Negative(), shouldBeValid, MessageKey.TimeSpanType.Negative); } public static IEnumerable NonNegative_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NonNegative(Convert)); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.TimeSpanType.NonNegative); } [Theory] [MemberData(nameof(NonNegative_Should_CollectError_Data))] public void NonNegative_Should_CollectError_FromNullable(TimeSpan model, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NonNegative(), shouldBeValid, MessageKey.TimeSpanType.NonNegative); } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Times/DateTimeOffsetRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Times { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Tests.Unit.Rules.Numbers; using Validot.Translations; using Xunit; public class DateTimeOffsetRulesTests { private static readonly Func Convert = ticks => new DateTimeOffset(ticks, TimeSpan.Zero); private static readonly TimesTestData.DateTimeConvert ConvertDateTimeOffset = (year, month, day, hour, minute, second, millisecond) => new DateTimeOffset(year, month, day, hour, minute, second, millisecond, TimeSpan.Zero); public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(DateTimeOffset model, DateTimeOffset value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(DateTimeOffset model, DateTimeOffset value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(DateTimeOffset model, DateTimeOffset value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(DateTimeOffset model, DateTimeOffset value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable After_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(After_Should_CollectError_Data))] public void After_Should_CollectError(DateTimeOffset model, DateTimeOffset min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(After_Should_CollectError_Data))] public void After_Should_CollectError_FromNullable(DateTimeOffset model, DateTimeOffset min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable AfterOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_Data))] public void AfterOrEqualTo_Should_CollectError(DateTimeOffset model, DateTimeOffset min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_Data))] public void AfterOrEqualTo_Should_CollectError_FromNullable(DateTimeOffset model, DateTimeOffset min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable Before_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(Before_Should_CollectError_Data))] public void Before_Should_CollectError(DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(Before_Should_CollectError_Data))] public void Before_Should_CollectError_FromNullable(DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable BeforeOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_Data))] public void BeforeOrEqualTo_Should_CollectError(DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_Data))] public void BeforeOrEqualTo_Should_CollectError_FromNullable(DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(DateTimeOffset.MinValue, DateTimeOffset.MaxValue, new DateTimeOffset(1, TimeSpan.Zero))); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public class ComparisonModesTests { public static IEnumerable EqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.EqualTo(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void EqualTo_Should_CollectError_When_TimeComparisonSet(DateTimeOffset model, DateTimeOffset value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void EqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset model, DateTimeOffset value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable NotEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.NotEqualTo(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void NotEqualTo_Should_CollectError_When_TimeComparisonSet(DateTimeOffset model, DateTimeOffset value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void NotEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset model, DateTimeOffset value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable After_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.After(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(After_Should_CollectError_When_TimeComparisonSet_Data))] public void After_Should_CollectError_When_TimeComparisonSet(DateTimeOffset model, DateTimeOffset min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min, timeComparison), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(After_Should_CollectError_When_TimeComparisonSet_Data))] public void After_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset model, DateTimeOffset min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min, timeComparison), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.AfterOrEqualTo(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet(DateTimeOffset model, DateTimeOffset min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min, timeComparison), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset model, DateTimeOffset min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min, timeComparison), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable Before_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.Before(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(Before_Should_CollectError_When_TimeComparisonSet_Data))] public void Before_Should_CollectError_When_TimeComparisonSet(DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max, timeComparison), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(Before_Should_CollectError_When_TimeComparisonSet_Data))] public void Before_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max, timeComparison), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.BeforeOrEqualTo(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet(DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max, timeComparison), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max, timeComparison), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable Between_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.Between(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(Between_Should_CollectError_When_TimeComparisonSet_Data))] public void Between_Should_CollectError_When_TimeComparisonSet(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max, timeComparison), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(Between_Should_CollectError_When_TimeComparisonSet_Data))] public void Between_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max, timeComparison), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.BetweenOrEqualTo(ConvertDateTimeOffset)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max, timeComparison), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTimeOffset min, DateTimeOffset model, DateTimeOffset max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max, timeComparison), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Times/DateTimeRulesTests.cs ================================================ namespace Validot.Tests.Unit.Rules.Times { using System; using System.Collections.Generic; using Validot.Testing; using Validot.Tests.Unit.Rules.Numbers; using Validot.Translations; using Xunit; public class DateTimeRulesTests { private static readonly Func Convert = ticks => new DateTime(ticks); private static readonly TimesTestData.DateTimeConvert ConvertDateTime = (year, month, day, hour, minute, second, millisecond) => new DateTime(year, month, day, hour, minute, second, millisecond); public static IEnumerable EqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.EqualTo_Unsigned(Convert), NumbersTestData.EqualTo_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError(DateTime model, DateTime value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_Data))] public void EqualTo_Should_CollectError_FromNullable(DateTime model, DateTime value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable NotEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.NotEqualTo_Unsigned(Convert), NumbersTestData.NotEqualTo_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError(DateTime model, DateTime value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_Data))] public void NotEqualTo_Should_CollectError_FromNullable(DateTime model, DateTime value, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable After_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThan_Unsigned(Convert), NumbersTestData.GreaterThan_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(After_Should_CollectError_Data))] public void After_Should_CollectError(DateTime model, DateTime min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(After_Should_CollectError_Data))] public void After_Should_CollectError_FromNullable(DateTime model, DateTime min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable AfterOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.GreaterThanOrEqualTo_Unsigned(Convert), NumbersTestData.GreaterThanOrEqualTo_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_Data))] public void AfterOrEqualTo_Should_CollectError(DateTime model, DateTime min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_Data))] public void AfterOrEqualTo_Should_CollectError_FromNullable(DateTime model, DateTime min, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable Before_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThan_Unsigned(Convert), NumbersTestData.LessThan_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(Before_Should_CollectError_Data))] public void Before_Should_CollectError(DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(Before_Should_CollectError_Data))] public void Before_Should_CollectError_FromNullable(DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable BeforeOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.LessThanOrEqualTo_Unsigned(Convert), NumbersTestData.LessThanOrEqualTo_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_Data))] public void BeforeOrEqualTo_Should_CollectError(DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_Data))] public void BeforeOrEqualTo_Should_CollectError_FromNullable(DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable Between_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.Between_Unsigned(Convert), NumbersTestData.Between_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError(DateTime min, DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(Between_Should_CollectError_Data))] public void Between_Should_CollectError_FromNullable(DateTime min, DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_Data() { return RulesHelper.GetTestDataCombined( NumbersTestData.BetweenOrEqualTo_Unsigned(Convert), NumbersTestData.BetweenOrEqualTo_Limits(DateTime.MinValue, DateTime.MaxValue, new DateTime(1))); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError(DateTime min, DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_Data))] public void BetweenOrEqualTo_Should_CollectError_FromNullable(DateTime min, DateTime model, DateTime max, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", TimeComparison.All)); } public class ComparisonModesTests { public static IEnumerable EqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.EqualTo(ConvertDateTime)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void EqualTo_Should_CollectError_When_TimeComparisonSet(DateTime model, DateTime value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(EqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void EqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime model, DateTime value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.EqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.EqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable NotEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.NotEqualTo(ConvertDateTime)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void NotEqualTo_Should_CollectError_When_TimeComparisonSet(DateTime model, DateTime value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(NotEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void NotEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime model, DateTime value, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.NotEqualTo(value, timeComparison), shouldBeValid, MessageKey.Times.NotEqualTo, Arg.Time("value", value), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable After_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.After(ConvertDateTime)); } [Theory] [MemberData(nameof(After_Should_CollectError_When_TimeComparisonSet_Data))] public void After_Should_CollectError_When_TimeComparisonSet(DateTime model, DateTime min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min, timeComparison), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(After_Should_CollectError_When_TimeComparisonSet_Data))] public void After_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime model, DateTime min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.After(min, timeComparison), shouldBeValid, MessageKey.Times.After, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.AfterOrEqualTo(ConvertDateTime)); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet(DateTime model, DateTime min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min, timeComparison), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void AfterOrEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime model, DateTime min, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.AfterOrEqualTo(min, timeComparison), shouldBeValid, MessageKey.Times.AfterOrEqualTo, Arg.Time("min", min), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable Before_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.Before(ConvertDateTime)); } [Theory] [MemberData(nameof(Before_Should_CollectError_When_TimeComparisonSet_Data))] public void Before_Should_CollectError_When_TimeComparisonSet(DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max, timeComparison), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(Before_Should_CollectError_When_TimeComparisonSet_Data))] public void Before_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Before(max, timeComparison), shouldBeValid, MessageKey.Times.Before, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.BeforeOrEqualTo(ConvertDateTime)); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet(DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max, timeComparison), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BeforeOrEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BeforeOrEqualTo(max, timeComparison), shouldBeValid, MessageKey.Times.BeforeOrEqualTo, Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable Between_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.Between(ConvertDateTime)); } [Theory] [MemberData(nameof(Between_Should_CollectError_When_TimeComparisonSet_Data))] public void Between_Should_CollectError_When_TimeComparisonSet(DateTime min, DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max, timeComparison), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(Between_Should_CollectError_When_TimeComparisonSet_Data))] public void Between_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime min, DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.Between(min, max, timeComparison), shouldBeValid, MessageKey.Times.Between, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } public static IEnumerable BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data() { return RulesHelper.GetTestDataCombined( TimesTestData.BetweenOrEqualTo(ConvertDateTime)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet(DateTime min, DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max, timeComparison), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } [Theory] [MemberData(nameof(BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_Data))] public void BetweenOrEqualTo_Should_CollectError_When_TimeComparisonSet_FromNullable(DateTime min, DateTime model, DateTime max, TimeComparison timeComparison, bool shouldBeValid) { Tester.TestSingleRule( model, m => m.BetweenOrEqualTo(min, max, timeComparison), shouldBeValid, MessageKey.Times.BetweenOrEqualTo, Arg.Time("min", min), Arg.Time("max", max), Arg.Enum("timeComparison", timeComparison)); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Rules/Times/TimesTestData.cs ================================================ namespace Validot.Tests.Unit.Rules.Times { using System.Collections.Generic; public static class TimesTestData { public delegate T DateTimeConvert(int year, int month, int day, int hour, int minute, int second, int millisecond); public static IEnumerable EqualTo(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // dates equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), TimeComparison.JustTime, false }; // times equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), TimeComparison.JustTime, true }; // all different yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 9, 5, 4, 3, 2, 9), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 9, 5, 4, 3, 2, 9), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 9, 5, 4, 3, 2, 9), TimeComparison.JustTime, false }; } public static IEnumerable NotEqualTo(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // dates equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), TimeComparison.JustTime, true }; // times equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), TimeComparison.JustTime, false }; // all different yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 9, 5, 4, 3, 2, 9), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 9, 5, 4, 3, 2, 9), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 9, 5, 4, 3, 2, 9), TimeComparison.JustTime, true }; } public static IEnumerable After(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // time before yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // time after yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // date before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // date after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // all before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // all after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; } public static IEnumerable AfterOrEqualTo(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // time before yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // time after yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // date before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // date after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // all before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // all after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; } public static IEnumerable Before(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // time before yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // time after yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // date before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // date after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // all before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // all after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; } public static IEnumerable BeforeOrEqualTo(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // time before yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // time after yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 5, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; // date before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // date after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 1), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // all before yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2007, 6, 1, 4, 3, 2, 0), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, true }; // all after yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2007, 6, 9, 4, 3, 2, 9), dateTimeConvert(2007, 6, 5, 4, 3, 2, 1), TimeComparison.JustTime, false }; } public static IEnumerable Between(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), TimeComparison.JustTime, false }; // all between yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates equal min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 9, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 9, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 9, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates equal max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // time equal min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // time equal max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 11), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 11), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 11), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // dates before min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates after max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // time before min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // time after max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // all before min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // all after max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; } public static IEnumerable BetweenOrEqualTo(DateTimeConvert dateTimeConvert) { // all equal yield return new object[] { dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), TimeComparison.JustTime, true }; // all between yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates equal min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 9, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 9, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 9, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates equal max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // time equal min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 9), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // time equal max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 11), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 11), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 11), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates before min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // dates after max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 10), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, true }; // time before min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // time after max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, true }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 10, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // all before min yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 8, 10, 10, 10, 8), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; // all after max yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.All, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustDate, false }; yield return new object[] { dateTimeConvert(2000, 10, 9, 10, 10, 10, 9), dateTimeConvert(2000, 10, 12, 10, 10, 10, 12), dateTimeConvert(2000, 10, 11, 10, 10, 10, 11), TimeComparison.JustTime, false }; } } } ================================================ FILE: tests/Validot.Tests.Unit/Settings/ValidatorSettingsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Settings { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Settings; using Validot.Tests.Unit.Translations; using Xunit; public class ValidatorSettingsExtensionsTests { public class WithTranslation_NameAndDictionary { [Fact] public void Should_ReturnSelf() { var validatorSettings = new ValidatorSettings(); var result = validatorSettings.WithTranslation("name", new Dictionary()); result.Should().BeSameAs(validatorSettings); } [Fact] public void Should_PassAllEntries() { var validatorSettings = new ValidatorSettings(); var dictionary = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", ["k3"] = "v3", ["k4"] = "v4", }; validatorSettings.WithTranslation("name", dictionary); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", ["k3"] = "v3", ["k4"] = "v4", }, }); } [Fact] public void Should_ThrowException_When_NullName() { var validatorSettings = new ValidatorSettings(); Action action = () => validatorSettings.WithTranslation(null, new Dictionary()); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullDictionary() { var validatorSettings = new ValidatorSettings(); Action action = () => validatorSettings.WithTranslation("name", null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullEntryInDictionary() { var validatorSettings = new ValidatorSettings(); var dictionary = new Dictionary() { ["k11"] = "v11", ["k12"] = null, ["k13"] = "v13", ["k14"] = "v14", }; Action action = () => validatorSettings.WithTranslation("name", dictionary); action.Should().ThrowExactly(); } [Fact] public void Should_PassAllEntries_FromMultipleDictionaries() { var validatorSettings = new ValidatorSettings(); var dictionary1 = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }; var dictionary2 = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", }; validatorSettings.WithTranslation("name1", dictionary1); validatorSettings.WithTranslation("name2", dictionary2); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", }, }); } } public class WithTranslation_DictionaryOfDictionaries { [Fact] public void Should_ReturnSelf() { var validatorSettings = new ValidatorSettings(); var result = validatorSettings.WithTranslation(new Dictionary>()); result.Should().BeSameAs(validatorSettings); } [Fact] public void Should_PassAllEntries() { var validatorSettings = new ValidatorSettings(); var dictionary = new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", ["k3"] = "v3", ["k4"] = "v4", } }; validatorSettings.WithTranslation(dictionary); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", ["k3"] = "v3", ["k4"] = "v4", }, }); } [Fact] public void Should_PassAllEntries_WhenMoreThanOneKeysAtTopLevelDefined() { var validatorSettings = new ValidatorSettings(); var dictionary = new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", } }; validatorSettings.WithTranslation(dictionary); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", }, }); } [Fact] public void Should_ThrowException_When_NullEntryInDictionary() { var validatorSettings = new ValidatorSettings(); var dictionary = new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = null, ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", } }; Action action = () => validatorSettings.WithTranslation(dictionary); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullDictionary() { var validatorSettings = new ValidatorSettings(); var dictionary = new Dictionary>() { ["name1"] = null, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", } }; Action action = () => validatorSettings.WithTranslation(dictionary); action.Should().ThrowExactly(); } [Fact] public void Should_PassAllEntries_MultipleTimes() { var validatorSettings = new ValidatorSettings(); var dictionary1 = new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", } }; var dictionary2 = new Dictionary>() { ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", } }; validatorSettings.WithTranslation(dictionary1); validatorSettings.WithTranslation(dictionary2); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name1"] = new Dictionary() { ["k11"] = "v11", ["k12"] = "v12", ["k13"] = "v13", ["k14"] = "v14", }, ["name2"] = new Dictionary() { ["k21"] = "v21", ["k22"] = "v22", ["k23"] = "v23", ["k24"] = "v24", }, }); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Settings/ValidatorSettingsTestHelpers.cs ================================================ namespace Validot.Tests.Unit.Settings { using FluentAssertions; using Validot.Settings; using Validot.Translations; public static class ValidatorSettingsTestHelpers { public static void ShouldBeLikeDefault(this IValidatorSettings @this) { @this.Should().NotBeNull(); @this.Translations.Keys.Should().HaveCount(1); @this.Translations.Keys.Should().Contain("English"); @this.Translations["English"].Should().NotBeNull(); foreach (var pair in Translation.English) { @this.Translations["English"].Keys.Should().Contain(pair.Key); @this.Translations["English"][pair.Key].Should().Be(pair.Value); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Settings/ValidatorSettingsTests.cs ================================================ namespace Validot.Tests.Unit.Settings { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Settings; using Validot.Tests.Unit.Translations; using Xunit; public class ValidatorSettingsTests { [Fact] public void Should_Initialize() { _ = new ValidatorSettings(); } [Fact] public void Should_Initialize_WithDefaultValues() { var validatorSettings = new ValidatorSettings(); validatorSettings.Translations.Should().BeEmpty(); validatorSettings.ReferenceLoopProtectionEnabled.Should().BeNull(); validatorSettings.IsLocked.Should().BeFalse(); } [Fact] public void Default_Should_HaveEnglishTranslation_And_DisabledStats() { var defaultSettings = ValidatorSettings.GetDefault(); defaultSettings.ShouldBeLikeDefault(); } public class ReferenceLoopProtection { [Fact] public void Should_WithReferenceLoopProtection_ReturnSelf() { var validatorSettings = new ValidatorSettings(); var result = validatorSettings.WithReferenceLoopProtection(); result.Should().BeSameAs(validatorSettings); } [Fact] public void Should_WithReferenceLoopProtection_Set_ReferenceLoopProtectionEnabled_To_True() { var validatorSettings = new ValidatorSettings(); validatorSettings.WithReferenceLoopProtection(); validatorSettings.ReferenceLoopProtectionEnabled.Should().BeTrue(); } [Fact] public void Should_WithReferenceLoopProtectionDisabled_ReturnSelf() { var validatorSettings = new ValidatorSettings(); var result = validatorSettings.WithReferenceLoopProtectionDisabled(); result.Should().BeSameAs(validatorSettings); } [Fact] public void Should_WithReferenceLoopProtectionDisabled_Set_ReferenceLoopProtectionEnabled_To_False() { var validatorSettings = new ValidatorSettings(); validatorSettings.WithReferenceLoopProtectionDisabled(); validatorSettings.ReferenceLoopProtectionEnabled.Should().BeFalse(); } [Fact] public void Should_WithReferenceLoopProtection_ThrowException_When_Locked() { var validatorSettings = new ValidatorSettings(); validatorSettings.IsLocked = true; Action action = () => { validatorSettings.WithReferenceLoopProtection(); }; action.Should().ThrowExactly(); } [Fact] public void Should_WithReferenceLoopProtectionDisabled_ThrowException_When_Locked() { var validatorSettings = new ValidatorSettings(); validatorSettings.IsLocked = true; Action action = () => { validatorSettings.WithReferenceLoopProtectionDisabled(); }; action.Should().ThrowExactly(); } } public class WithTranslation { [Fact] public void Should_ReturnSelf() { var validatorSettings = new ValidatorSettings(); var result = validatorSettings.WithTranslation("name", "key", "translation"); result.Should().BeSameAs(validatorSettings); } [Fact] public void Should_AddEntry() { var validatorSettings = new ValidatorSettings(); validatorSettings.WithTranslation("name", "k1", "v1"); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name"] = new Dictionary() { ["k1"] = "v1", }, }); } [Fact] public void Should_AddMultipleEntries() { var validatorSettings = new ValidatorSettings(); validatorSettings.WithTranslation("name1", "k1", "v1"); validatorSettings.WithTranslation("name1", "k2", "v2"); validatorSettings.WithTranslation("name2", "k3", "v3"); validatorSettings.WithTranslation("name2", "k4", "v4"); validatorSettings.Translations.ShouldBeLikeTranslations( new Dictionary>() { ["name1"] = new Dictionary() { ["k1"] = "v1", ["k2"] = "v2", }, ["name2"] = new Dictionary() { ["k3"] = "v3", ["k4"] = "v4", }, }); } [Theory] [InlineData(null, "key", "value")] [InlineData("name", null, "value")] [InlineData("name", "key", null)] public void Should_ThrowException_When_NullArgument(string name, string key, string translation) { var validatorSettings = new ValidatorSettings(); Action action = () => validatorSettings.WithTranslation(name, key, translation); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Locked() { var validatorSettings = new ValidatorSettings(); validatorSettings.IsLocked = true; Action action = () => { validatorSettings.WithTranslation("a", "b", "c"); }; action.Should().ThrowExactly(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AndExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AndExtensionTests { [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(IRuleIn) }); } [Fact] public void Should_NotAddAnyCommand() { Func, ISpecificationOut> fluentApi = s => s.Optional().And().Rule(x => true).And().AsModel(m => m); var result = fluentApi(new SpecificationApi()); var processedApi = (SpecificationApi)result; processedApi.Commands.Count.Should().Be(3); processedApi.Commands[0].Should().BeOfType(); processedApi.Commands[1].Should().BeOfType>(); processedApi.Commands[2].Should().BeOfType>(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/ApiTester.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Specification; internal static class ApiTester { internal static void TestOutputPossibilities(IReadOnlyList types) { var followedBy = typeof(TOut).GetInterfaces(); followedBy.Length.Should().Be(types.Count); followedBy.Should().Contain(types); } internal static void TestSingleCommand(Func fluentApi, Action validateCommand = null) where TIn : class where TCommand : class { var api = new SpecificationApi(); api.Should().BeAssignableTo(); var result = fluentApi(api as TIn); result.Should().BeSameAs(api); var processedApi = result as SpecificationApi; processedApi.Commands.Count.Should().Be(1); var command = processedApi.Commands.Single(); command.Should().NotBeNull(); command.Should().BeOfType(); if (validateCommand != null) { validateCommand(command as TCommand); } } internal static void TextException(Func fluentApi, Action addingAction = null) where TIn : class { var api = new SpecificationApi(); api.Should().BeAssignableTo(); Action action = () => fluentApi(api as TIn); action.Should().Throw(); if (addingAction != null) { addingAction(action); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsCollectionExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsCollectionExtensionTests { public class IEnumerableCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class ArrayCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleOut, AsCollectionCommand>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleOut>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class ICollectionCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IReadOnlyCollectionCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IListCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IReadOnlyListCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class ListCollection { [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } [Fact] public void Should_Add_AsCollectionCommand() { Specification specification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsCollectionCommand, object>>( s => s.AsCollection, object>(specification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(specification); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsCollection, object>(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsConvertedExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Linq.Expressions; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsConvertedExtensionTests { private class SourceClass { } private class TargetClass { } [Fact] public void Should_Add_ConvertedCommand() { Converter converter = s => new TargetClass(); Specification targetSpecifiction = s => s; ApiTester.TestSingleCommand, IRuleOut, AsConvertedCommand>( s => s.AsConverted(converter, targetSpecifiction), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(targetSpecifiction); command.Converter.Should().NotBeNull(); command.Converter.Should().BeSameAs(converter); }); } [Fact] public void Should_ThrowException_When_NullConvert() { Specification targetSpecifiction = s => s; ApiTester.TextException, IRuleOut>( s => s.AsConverted(null, targetSpecifiction), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullTargetSpecification() { Converter converter = s => new TargetClass(); ApiTester.TextException, IRuleOut>( s => s.AsConverted(converter, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsDictionaryExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Collections.Generic; using System.Globalization; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsDictionaryExtensionTests { public class IEnumerable { [Fact] public void Should_Add_DictionaryCommand() { Func keyStringifier = i => i.ToString(CultureInfo.InvariantCulture); Specification valueSpecification = s => s; ApiTester.TestSingleCommand>, IRuleIn>>, IRuleOut>>, AsDictionaryCommand>, int, object>>( s => s.AsDictionary(valueSpecification, keyStringifier), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().NotBeNull(); command.KeyStringifier.Should().BeSameAs(keyStringifier); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException>, IRuleIn>>, IRuleOut>>>( s => s.AsDictionary>, int, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullStringifier() { ApiTester.TextException>, IRuleIn>>, IRuleOut>>>( s => s.AsDictionary>, int, object>(s1 => s1, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IReadOnlyCollection { [Fact] public void Should_Add_DictionaryCommand() { Func keyStringifier = i => i.ToString(CultureInfo.InvariantCulture); Specification valueSpecification = s => s; ApiTester.TestSingleCommand>, IRuleIn>>, IRuleOut>>, AsDictionaryCommand>, int, object>>( s => s.AsDictionary(valueSpecification, keyStringifier), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().NotBeNull(); command.KeyStringifier.Should().BeSameAs(keyStringifier); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException>, IRuleIn>>, IRuleOut>>>( s => s.AsDictionary>, int, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullStringifier() { ApiTester.TextException>, IRuleIn>>, IRuleOut>>>( s => s.AsDictionary>, int, object>(s1 => s1, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class Dictionary { [Fact] public void Should_Add_DictionaryCommand() { Func keyStringifier = i => i.ToString(CultureInfo.InvariantCulture); Specification valueSpecification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsDictionaryCommand, int, object>>( s => s.AsDictionary(valueSpecification, keyStringifier), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().NotBeNull(); command.KeyStringifier.Should().BeSameAs(keyStringifier); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, int, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullStringifier() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, int, object>(s1 => s1, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IDictionary { [Fact] public void Should_Add_DictionaryCommand() { Func keyStringifier = i => i.ToString(CultureInfo.InvariantCulture); Specification valueSpecification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsDictionaryCommand, int, object>>( s => s.AsDictionary(valueSpecification, keyStringifier), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().NotBeNull(); command.KeyStringifier.Should().BeSameAs(keyStringifier); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, int, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullStringifier() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, int, object>(s1 => s1, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IReadOnlyDictionary { [Fact] public void Should_Add_DictionaryCommand() { Func keyStringifier = i => i.ToString(CultureInfo.InvariantCulture); Specification valueSpecification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsDictionaryCommand, int, object>>( s => s.AsDictionary(valueSpecification, keyStringifier), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().NotBeNull(); command.KeyStringifier.Should().BeSameAs(keyStringifier); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, int, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullStringifier() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, int, object>(s1 => s1, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsDictionaryWithKeyStringExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Collections.Generic; using System.Globalization; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsDictionaryWithKeyStringExtensionTests { public class IEnumerable { [Fact] public void Should_Add_DictionaryCommand() { Specification valueSpecification = s => s; ApiTester.TestSingleCommand>, IRuleIn>>, IRuleOut>>, AsDictionaryCommand>, string, object>>( s => s.AsDictionary(valueSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().BeNull(); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException>, IRuleIn>>, IRuleOut>>>( s => s.AsDictionary>, string, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IReadOnlyCollection { [Fact] public void Should_Add_DictionaryCommand() { Specification valueSpecification = s => s; ApiTester.TestSingleCommand>, IRuleIn>>, IRuleOut>>, AsDictionaryCommand>, string, object>>( s => s.AsDictionary(valueSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().BeNull(); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException>, IRuleIn>>, IRuleOut>>>( s => s.AsDictionary>, string, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class Dictionary { [Fact] public void Should_Add_DictionaryCommand() { Specification valueSpecification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsDictionaryCommand, string, object>>( s => s.AsDictionary(valueSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().BeNull(); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, string, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IDictionary { [Fact] public void Should_Add_DictionaryCommand() { Specification valueSpecification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsDictionaryCommand, string, object>>( s => s.AsDictionary(valueSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().BeNull(); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, string, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } } public class IReadOnlyDictionary { [Fact] public void Should_Add_DictionaryCommand() { Specification valueSpecification = s => s; ApiTester.TestSingleCommand, IRuleIn>, IRuleOut>, AsDictionaryCommand, string, object>>( s => s.AsDictionary(valueSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(valueSpecification); command.KeyStringifier.Should().BeNull(); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleIn>, IRuleOut>>( s => s.AsDictionary, string, object>(null, i => i.ToString(CultureInfo.InvariantCulture)), addingAction => { addingAction.Should().ThrowExactly(); }); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsModelExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsModelExtensionTests { [Fact] public void Should_Add_AsModelCommand() { Specification modelSpecification = s => s; ApiTester.TestSingleCommand, IRuleOut, AsModelCommand>( s => s.AsModel(modelSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(modelSpecification); }); } [Fact] public void Should_ThrowException_When_NullModelSpecification() { ApiTester.TextException, IRuleOut>( s => s.AsModel(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsNullableExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsNullableExtensionTests { [Fact] public void Should_Add_AsNullableCommand() { Specification modelSpecification = s => s; ApiTester.TestSingleCommand, IRuleOut, AsNullableCommand>( s => s.AsNullable(modelSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(modelSpecification); }); } [Fact] public void Should_ThrowException_When_NullModelSpecification() { ApiTester.TextException, IRuleOut>( s => s.AsNullable(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/AsTypeExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Linq.Expressions; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class AsTypeExtensionTests { private class SourceClass { } private class TargetClass { } [Fact] public void Should_Add_AsTypeCommand() { Specification targetSpecifiction = s => s; ApiTester.TestSingleCommand, IRuleOut, AsTypeCommand>( s => s.AsType(targetSpecifiction), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(targetSpecifiction); }); } [Fact] public void Should_ThrowException_When_NullSpecification() { ApiTester.TextException, IRuleOut>( s => s.AsType(null as Specification), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/AsCollectionCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using System.Collections.Generic; using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class AsCollectionCommandTests { [Fact] public void Should_Get_BlockBuilder() { Specification specification = s => s; var command = new AsCollectionCommand, object>(specification); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification_And_AddModelBlock() { Specification specification = s => s; var command = new AsCollectionCommand, object>(specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType, object>>(); var modelBlock = (CollectionCommandScope, object>)block; modelBlock.ScopeId.Should().Be(666); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/AsConvertedCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using System; using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class AsConvertedCommandTests { private class SourceClass { } private class TargetClass { } [Fact] public void Should_Get_BlockBuilder() { Specification specification = s => s; var command = new AsConvertedCommand(m => new TargetClass(), specification); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification_And_SaveConverter() { Specification specification = s => s; Converter converter = _ => new TargetClass(); var command = new AsConvertedCommand(converter, specification); var scopeBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var scope = scopeBuilder.Build(buildingContext); scope.Should().BeOfType>(); var convertedCommandscope = scope as ConvertedCommandScope; convertedCommandscope.ScopeId.Should().Be(666); convertedCommandscope.Converter.Should().BeSameAs(converter); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/AsDictionaryCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using System; using System.Collections.Generic; using System.Globalization; using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class AsDictionaryCommandTests { [Fact] public void Should_Get_ScopeBuilder() { Specification specification = s => s; Func keyStringifier = i => i.ToString(CultureInfo.InvariantCulture); var command = new AsDictionaryCommand, int, object>(specification, keyStringifier); var scopeBuilder = command.GetScopeBuilder(); scopeBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification_And_AddModelBlock() { Specification specification = s => s; var command = new AsDictionaryCommand, int, object>(specification, i => i.ToString(CultureInfo.InvariantCulture)); var scopeBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var scope = scopeBuilder.Build(buildingContext); scope.Should().BeOfType, int, object>>(); var modelScope = (DictionaryCommandScope, int, object>)scope; modelScope.ScopeId.Should().Be(666); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/AsModelCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class AsModelCommandTests { [Fact] public void Should_Get_BlockBuilder() { Specification specification = s => s; var command = new AsModelCommand(specification); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification_And_AddModelBlock() { Specification specification = s => s; var command = new AsModelCommand(specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var modelBlock = (ModelCommandScope)block; modelBlock.ScopeId.Should().Be(666); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/AsNullableCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class AsNullableCommandTests { [Fact] public void Should_Get_BlockBuilder() { Specification specification = s => s; var command = new AsNullableCommand(specification); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification_And_AddModelBlock() { Specification specification = s => s; var command = new AsNullableCommand(specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var modelBlock = block as NullableCommandScope; modelBlock.ScopeId.Should().Be(666); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/AsTypeCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using System; using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class AsTypeCommandTests { private class SourceClass { } private class TargetClass { } [Fact] public void Should_Get_BlockBuilder() { Specification specification = s => s; var command = new AsTypeCommand(specification); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification() { Specification specification = s => s; var command = new AsTypeCommand(specification); var scopeBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var scope = scopeBuilder.Build(buildingContext); scope.Should().BeOfType>(); var typeCommandScope = scope as TypeCommandScope; typeCommandScope.ScopeId.Should().Be(666); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/MemberCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using System; using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class MemberCommandTests { private class SomeMember { public int NestedValue { get; set; } public Type NestedObject { get; set; } } private class SomeModel { public object SomeReferenceProperty { get; set; } public int SomeValueProperty { get; set; } public object SomeReferenceVariable; public int SomeValueVariable; public object SomeFunctionReturningReference() => null; public int SomeFunctionReturningValue() => 888; public SomeMember Member { get; set; } } [Fact] public void Should_Get_BlockBuilder() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeReferenceProperty, specification); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); } [Fact] public void Should_GetOrRegisterSpecification_And_ModelBlock() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeReferenceProperty, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))).Returns(666); var block = blockBuilder.Build(buildingContext); buildingContext.Received(1).GetOrRegisterSpecificationScope(Arg.Is>(arg => ReferenceEquals(arg, specification))); block.Should().BeOfType>(); var modelBlock = (MemberCommandScope)block; modelBlock.ScopeId.Should().Be(666); } [Fact] public void Should_Process_Reference_Property() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeReferenceProperty, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var modelBlock = (MemberCommandScope)block; modelBlock.Path = "SomeReferenceProperty"; modelBlock.GetMemberValue.Should().BeOfType>(); var someModel = new SomeModel() { SomeReferenceProperty = new object() }; var memberValue = modelBlock.GetMemberValue(someModel); memberValue.Should().BeSameAs(someModel.SomeReferenceProperty); } [Fact] public void Should_Process_Reference_Variable() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeReferenceVariable, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var modelBlock = (MemberCommandScope)block; modelBlock.Path = "SomeReferenceVariable"; modelBlock.GetMemberValue.Should().BeOfType>(); var someModel = new SomeModel() { SomeReferenceVariable = new object() }; var memberValue = modelBlock.GetMemberValue(someModel); memberValue.Should().BeSameAs(someModel.SomeReferenceVariable); } [Fact] public void Should_Process_ValueType_Property() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeValueProperty, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var modelBlock = (MemberCommandScope)block; modelBlock.Path = "SomeValueProperty"; modelBlock.GetMemberValue.Should().BeOfType>(); var someModel = new SomeModel() { SomeValueProperty = 777 }; var memberValue = modelBlock.GetMemberValue(someModel); memberValue.Should().Be(777); } [Fact] public void Should_Process_ValueType_Variable() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeValueVariable, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var modelBlock = (MemberCommandScope)block; modelBlock.Path = "SomeValueVariable"; modelBlock.GetMemberValue.Should().BeOfType>(); var someModel = new SomeModel() { SomeValueVariable = 777 }; var memberValue = modelBlock.GetMemberValue(someModel); memberValue.Should().Be(777); } [Fact] public void Should_ThrowException_When_MemberSelectorPointsMoreThanOneLevelDown_TwoLevels() { Specification specification = s => s; var command = new MemberCommand(m => m.Member.NestedValue, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); Action action = () => blockBuilder.Build(buildingContext); action.Should().ThrowExactly().WithMessage("Only one level of nesting is allowed, m => m.Member.NestedValue looks like it is going further (member of a member?)"); } [Fact] public void Should_ThrowException_When_MemberSelectorPointsMoreThanOneLevelDown_ThreeLevels() { Specification specification = s => s; var command = new MemberCommand(m => m.Member.NestedObject.FullName, specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); Action action = () => blockBuilder.Build(buildingContext); action.Should().ThrowExactly().WithMessage("Only one level of nesting is allowed, m => m.Member.NestedObject.FullName looks like it is going further (member of a member?)"); } [Fact] public void Should_ThrowException_When_MemberIsFunction_ReturningValueType() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeFunctionReturningValue(), specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); Action action = () => blockBuilder.Build(buildingContext); action.Should().ThrowExactly().WithMessage("Only properties and variables are valid members to validate, m => m.SomeFunctionReturningValue() looks like it is pointing at something else (a method?)."); } [Fact] public void Should_ThrowException_When_MemberIsFunction_ReturningReferenceType() { Specification specification = s => s; var command = new MemberCommand(m => m.SomeFunctionReturningReference(), specification); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); buildingContext.GetOrRegisterSpecificationScope(Arg.Any>()).Returns(666); Action action = () => blockBuilder.Build(buildingContext); action.Should().ThrowExactly().WithMessage("Only properties and variables are valid members to validate, m => m.SomeFunctionReturningReference() looks like it is pointing at something else (a method?)."); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/Commands/RuleCommandTests.cs ================================================ namespace Validot.Tests.Unit.Specification.Commands { using System; using FluentAssertions; using NSubstitute; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class RuleCommandTests { [Fact] public void Should_Get_RuleBlockBuilder() { Predicate predicate = x => true; var command = new RuleCommand(predicate); var blockBuilder = command.GetScopeBuilder(); blockBuilder.Should().NotBeNull(); blockBuilder.Should().BeAssignableTo>(); } [Fact] public void Should_GetRuleBlock_With_Predicate() { Predicate predicate = x => true; var command = new RuleCommand(predicate); var blockBuilder = command.GetScopeBuilder(); var buildingContext = Substitute.For(); var block = blockBuilder.Build(buildingContext); block.Should().BeOfType>(); var ruleBlock = (RuleCommandScope)block; ruleBlock.IsValid.Should().BeSameAs(predicate); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/ForbiddenExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class ForbiddenExtensionTests { [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IWithMessageForbiddenIn), typeof(IWithExtraMessageForbiddenIn), typeof(IWithCodeForbiddenIn), typeof(IWithExtraCodeForbiddenIn), }); } [Fact] public void Should_Forbidden_Add_ForbiddenCommand() { ApiTester.TestSingleCommand, IForbiddenOut, ForbiddenCommand>( s => s.Forbidden()); } [Fact] public void Should_Forbidden_Add_ForbiddenCommand_When_Nullable() { ApiTester.TestSingleCommand, IForbiddenOut, ForbiddenCommand>( s => s.Forbidden()); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/MemberExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using System.Linq.Expressions; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class MemberExtensionTests { private class TestClass { public string TestProperty { get; set; } } [Fact] public void Should_Add_MemberCommand() { Specification memberSpecification = s => s; Expression> memberSelector = m => m.TestProperty; ApiTester.TestSingleCommand, IRuleOut, MemberCommand>( s => s.Member(memberSelector, memberSpecification), command => { command.Specification.Should().NotBeNull(); command.Specification.Should().BeSameAs(memberSpecification); command.MemberSelector.Should().NotBeNull(); command.MemberSelector.Should().BeSameAs(memberSelector); }); } [Fact] public void Should_ThrowException_When_NullMemberSelector() { Specification memberSpecification = s => s; ApiTester.TextException, IRuleOut>( s => s.Member(null, memberSpecification), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_ThrowException_When_NullMemberSpecification() { Expression> memberSelector = m => m.TestProperty; ApiTester.TextException, IRuleOut>( s => s.Member(memberSelector, null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/OptionalExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class OptionalExtensionTests { [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IAndIn) }); } [Fact] public void Should_Add_OptionalCommand() { ApiTester.TestSingleCommand, IOptionalOut, OptionalCommand>( s => s.Optional()); } [Fact] public void Should_Add_OptionalCommand_When_Nullable() { ApiTester.TestSingleCommand, IOptionalOut, OptionalCommand>( s => s.Optional()); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/RequiredExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class RequiredExtensionTests { [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithMessageIn), typeof(IWithExtraMessageIn), typeof(IWithCodeIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_Required_Add_RequiredCommand() { ApiTester.TestSingleCommand, IRequiredOut, RequiredCommand>( s => s.Required()); } [Fact] public void Should_Required_Add_RequiredCommand_When_Nullable() { ApiTester.TestSingleCommand, IRequiredOut, RequiredCommand>( s => s.Required()); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/RuleExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class RuleExtensionTests { [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithConditionIn), typeof(IWithPathIn), typeof(IWithMessageIn), typeof(IWithExtraMessageIn), typeof(IWithCodeIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_Rule_Add_Command() { Predicate predicate = x => true; ApiTester.TestSingleCommand, IRuleIn, RuleCommand>( s => s.Rule(predicate), command => { command.Args.Should().BeNull(); command.Message.Should().BeNull(); command.Predicate.Should().BeSameAs(predicate); }); } [Fact] public void Should_Rule_ThrowException_When_NullPredicate() { ApiTester.TextException, IRuleIn>( s => s.Rule(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_RuleTemplate_Add_Command() { Predicate predicate = x => true; var args = new[] { Arg.Text("1", "1"), Arg.Number("2", 2), Arg.Type("3", typeof(Guid)) }; ApiTester.TestSingleCommand, IRuleIn, RuleCommand>( s => s.RuleTemplate(predicate, "messageKey", args), command => { command.Args.Should().BeSameAs(args); command.Args.Count.Should().Be(3); command.Args.Should().Contain(args); command.Message.Should().Be("messageKey"); command.Predicate.Should().BeSameAs(predicate); }); } [Fact] public void Should_RuleTemplate_Add_Command_WithoutArgs() { Predicate predicate = x => true; ApiTester.TestSingleCommand, IRuleIn, RuleCommand>( s => s.RuleTemplate(predicate, "messageKey", null), command => { command.Args.Should().BeNull(); command.Message.Should().Be("messageKey"); command.Predicate.Should().BeSameAs(predicate); }); } [Fact] public void Should_RuleTemplate_ThrowException_When_NullPredicate() { var args = new[] { Arg.Text("1", "1"), Arg.Number("2", 2), Arg.Type("3", typeof(Guid)) }; ApiTester.TextException, IRuleIn>( s => s.RuleTemplate(null, "message", args), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_RuleTemplate_ThrowException_When_NullMessage() { Predicate predicate = x => true; var args = new[] { Arg.Text("1", "1"), Arg.Number("2", 2), Arg.Type("3", typeof(Guid)) }; ApiTester.TextException, IRuleIn>( s => s.RuleTemplate(predicate, null, args), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_RuleTemplate_ThrowException_When_NullInArgs() { Predicate predicate = x => true; var args = new[] { Arg.Text("1", "1"), null, Arg.Type("3", typeof(Guid)) }; ApiTester.TextException, IRuleIn>( s => s.RuleTemplate(predicate, "message", args), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/WithCodeExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class WithCodeExtensionTests { [Fact] public void Should_ForbiddenWithCode_Add_WithCodeCommand() { ApiTester.TestSingleCommand, IWithCodeForbiddenOut, WithCodeCommand>( s => s.WithCode("code"), command => { command.Code.Should().NotBeNull(); command.Code.Should().Be("code"); }); } [Fact] public void Should_ForbiddenWithCode_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IWithExtraCodeForbiddenIn) }); } [Fact] public void Should_ForbiddenWithCode_ThrowException_When_NullMessage() { ApiTester.TextException, IWithCodeForbiddenOut>( s => s.WithCode(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_WithCode_Add_WithCodeCommand() { ApiTester.TestSingleCommand, IWithCodeOut, WithCodeCommand>( s => s.WithCode("code"), command => { command.Code.Should().NotBeNull(); command.Code.Should().Be("code"); }); } [Fact] public void Should_WithCode_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_WithCode_ThrowException_When_NullMessage() { ApiTester.TextException, IWithCodeOut>( s => s.WithCode(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Theory] [MemberData(nameof(CodeHelperTests.Codes_Valid), MemberType = typeof(CodeHelperTests))] public void Should_Accept_ValidCodes(string code) { ApiTester.TestSingleCommand, IWithCodeOut, WithCodeCommand>( s => s.WithCode(code), command => { command.Code.Should().Be(code); }); } [Theory] [MemberData(nameof(CodeHelperTests.Codes_Invalid), MemberType = typeof(CodeHelperTests))] public void Should_ThrowException_When_InvalidCodes(string code) { ApiTester.TextException, IWithCodeOut>( s => s.WithCode(code), addingAction => { addingAction.Should().ThrowExactly().WithMessage("Invalid code*"); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/WithConditionExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class WithConditionExtensionTests { [Fact] public void Should_Add_WithConditionCommand() { Predicate predicate = x => true; ApiTester.TestSingleCommand, IWithConditionOut, WithConditionCommand>( s => s.WithCondition(predicate), command => { command.ExecutionCondition.Should().NotBeNull(); command.ExecutionCondition.Should().BeSameAs(predicate); }); } [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithPathIn), typeof(IWithMessageIn), typeof(IWithExtraMessageIn), typeof(IWithCodeIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_ThrowException_When_NullExecutionCondition() { ApiTester.TextException, IWithConditionOut>( s => s.WithCondition(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/WithExtraCodeExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class WithExtraCodeExtensionTests { [Fact] public void Should_ForbiddenWithExtraCode_Add_WithExtraCodeCommand() { ApiTester.TestSingleCommand, IWithExtraCodeForbiddenOut, WithExtraCodeCommand>( s => s.WithExtraCode("code"), command => { command.Code.Should().NotBeNull(); command.Code.Should().Be("code"); }); } [Fact] public void Should_ForbiddenWithExtraCode_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IWithExtraCodeForbiddenIn) }); } [Fact] public void Should_ForbiddenWithExtraCode_ThrowException_When_NullMessage() { ApiTester.TextException, IWithExtraCodeForbiddenOut>( s => s.WithExtraCode(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_WithExtraCode_Add_WithExtraCodeCommand() { ApiTester.TestSingleCommand, IWithExtraCodeOut, WithExtraCodeCommand>( s => s.WithExtraCode("code"), command => { command.Code.Should().NotBeNull(); command.Code.Should().Be("code"); }); } [Fact] public void Should_WithExtraCode_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_WithExtraCode_ThrowException_When_NullMessage() { ApiTester.TextException, IWithExtraCodeOut>( s => s.WithExtraCode(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Theory] [MemberData(nameof(CodeHelperTests.Codes_Valid), MemberType = typeof(CodeHelperTests))] public void Should_Accept_ValidCodes(string code) { ApiTester.TestSingleCommand, IWithExtraCodeOut, WithExtraCodeCommand>( s => s.WithExtraCode(code), command => { command.Code.Should().Be(code); }); } [Theory] [MemberData(nameof(CodeHelperTests.Codes_Invalid), MemberType = typeof(CodeHelperTests))] public void Should_ThrowException_When_InvalidCodes(string code) { ApiTester.TextException, IWithExtraCodeOut>( s => s.WithExtraCode(code), addingAction => { addingAction.Should().ThrowExactly().WithMessage("Invalid code*"); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/WithExtraMessageExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class WithExtraMessageExtensionTests { [Fact] public void Should_ForbiddenWithExtraMessage_Add_WithExtraMessageCommand() { ApiTester.TestSingleCommand, IWithExtraMessageForbiddenOut, WithExtraMessageCommand>( s => s.WithExtraMessage("message"), command => { command.Message.Should().NotBeNull(); command.Message.Should().Be("message"); }); } [Fact] public void Should_ForbiddenWithExtraMessage_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IWithExtraMessageForbiddenIn), typeof(IWithExtraCodeForbiddenIn) }); } [Fact] public void Should_ForbiddenWithExtraMessage_ThrowException_When_NullMessage() { ApiTester.TextException, IWithExtraMessageForbiddenOut>( s => s.WithExtraMessage(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_WithExtraMessage_Add_WithExtraMessageCommand() { ApiTester.TestSingleCommand, IWithExtraMessageOut, WithExtraMessageCommand>( s => s.WithExtraMessage("message"), command => { command.Message.Should().NotBeNull(); command.Message.Should().Be("message"); }); } [Fact] public void Should_WithExtraMessage_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithExtraMessageIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_WithExtraMessage_ThrowException_When_NullMessage() { ApiTester.TextException, IWithExtraMessageOut>( s => s.WithExtraMessage(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/WithMessageExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class WithMessageExtensionTests { [Fact] public void Should_ForbiddenWithMessage_Add_WithMessageCommand() { ApiTester.TestSingleCommand, IWithMessageForbiddenOut, WithMessageCommand>( s => s.WithMessage("message"), command => { command.Message.Should().NotBeNull(); command.Message.Should().Be("message"); }); } [Fact] public void Should_ForbiddenWithMessage_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IWithExtraMessageForbiddenIn), typeof(IWithExtraCodeForbiddenIn) }); } [Fact] public void Should_ForbiddenWithMessage_ThrowException_When_NullMessage() { ApiTester.TextException, IWithMessageForbiddenOut>( s => s.WithMessage(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Fact] public void Should_WithMessage_Add_WithMessageCommand() { ApiTester.TestSingleCommand, IWithMessageOut, WithMessageCommand>( s => s.WithMessage("message"), command => { command.Message.Should().NotBeNull(); command.Message.Should().Be("message"); }); } [Fact] public void Should_WithMessage_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithExtraMessageIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_WithMessage_ThrowException_When_NullMessage() { ApiTester.TextException, IWithMessageOut>( s => s.WithMessage(null), addingAction => { addingAction.Should().ThrowExactly(); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Specification/WithPathExtensionTests.cs ================================================ namespace Validot.Tests.Unit.Specification { using System; using FluentAssertions; using Validot.Specification; using Validot.Specification.Commands; using Xunit; public class WithPathExtensionTests { [Fact] public void Should_BeEntryPoint() { ApiTester.TestOutputPossibilities>(new[] { typeof(ISpecificationOut), typeof(IRuleIn), typeof(IWithMessageIn), typeof(IWithExtraMessageIn), typeof(IWithCodeIn), typeof(IWithExtraCodeIn), typeof(IAndIn) }); } [Fact] public void Should_Add_WithPathCommand() { ApiTester.TestSingleCommand, IWithPathOut, WithPathCommand>( s => s.WithPath("path"), command => { command.Path.Should().Be("path"); }); } [Fact] public void Should_ThrowException_When_NullName() { ApiTester.TextException, IWithPathOut>( s => s.WithPath(null), addingAction => { addingAction.Should().ThrowExactly(); }); } [Theory] [MemberData(nameof(PathTestData.ValidPaths), MemberType = typeof(PathTestData))] public void Should_Accept_ValidPaths(string path) { ApiTester.TestSingleCommand, IWithPathOut, WithPathCommand>( s => s.WithPath(path), command => { command.Path.Should().Be(path); }); } [Theory] [MemberData(nameof(PathTestData.InvalidPaths), MemberType = typeof(PathTestData))] public void Should_ReturnFalse_For_InvalidPaths(string path) { ApiTester.TextException, IWithPathOut>( s => s.WithPath(path), addingAction => { addingAction.Should().ThrowExactly().WithMessage("Invalid path*"); }); } } } ================================================ FILE: tests/Validot.Tests.Unit/TempTests.cs ================================================ namespace Validot.Tests.Unit { using Xunit; public class TempTests { [Fact] public void Hey() { } } } ================================================ FILE: tests/Validot.Tests.Unit/Testing/TesterTests.cs ================================================ namespace Validot.Tests.Unit.Testing { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Errors; using Validot.Testing; using Xunit; public class TesterTests { public class TestExceptionOnInit { [Fact] public void Should_ThrowException_When_NullSpecification() { Action action = () => Tester.TestExceptionOnInit((Specification)null, typeof(InvalidOperationException)); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullType() { var specification = new Specification(m => m); Action action = () => Tester.TestExceptionOnInit(specification, null); action.Should().ThrowExactly(); } [Fact] public void Should_RethrowExceptionThrownInSpecification() { var exception = new InvalidOperationException(); var specification = new Specification(m => throw exception); Exception result = Tester.TestExceptionOnInit(specification, typeof(InvalidOperationException)); result.Should().BeSameAs(exception); } [Fact] public void Should_RethrowExceptionThrownInSpecification_When_ExpectedExceptionTypeDerivesFromThrown() { var exception = new InvalidOperationException(); var specification = new Specification(m => throw exception); Exception result = Tester.TestExceptionOnInit(specification, typeof(Exception)); result.Should().BeSameAs(exception); } [Fact] public void Should_ThrowTestFailedException_When_DifferentTypeOfExceptionIsThrownFromSpecification() { var exception = new InvalidOperationException(); var specification = new Specification(m => throw exception); Action action = () => Tester.TestExceptionOnInit(specification, typeof(ArgumentNullException)); action.Should().ThrowExactly().WithMessage($"Exception of type {typeof(ArgumentNullException).FullName} was expected, but found {typeof(InvalidOperationException).FullName}."); } [Fact] public void Should_ThrowTestFailedException_When_NoExceptionThrownFromSpecification() { var specification = new Specification(m => m); Action action = () => Tester.TestExceptionOnInit(specification, typeof(InvalidOperationException)); action.Should().ThrowExactly().WithMessage($"Exception of type {typeof(InvalidOperationException).FullName} was expected, but no exception has been thrown."); } } public class TestSpecification { [Fact] public void Should_ExecuteValidationOnObject_When_ObjectIsReferenceType() { var model = new object(); var tested = false; object testedModel = null; Tester.TestSpecification( model, s => s.Rule(m => { tested = true; testedModel = m; return true; })); tested.Should().BeTrue(); testedModel.Should().BeSameAs(model); } [Fact] public void Should_ExecuteValidationOnObject_When_ObjectIsValueType() { Guid model = Guid.NewGuid(); var tested = false; Guid testedModel = Guid.Empty; Tester.TestSpecification( model, s => s.Rule(m => { tested = true; testedModel = m; return true; })); tested.Should().BeTrue(); testedModel.Should().Be(model); } [Fact] public void Should_ReturnFailure_When_Error_Arg_InvalidName() { Specification specification = s => s .RuleTemplate(r => false, "key1", Arg.Text("arg1", "argValue1")).WithPath("member1") .RuleTemplate(r => false, "key21", Arg.Text("arg21", "argValue21")).WithPath("member2") .RuleTemplate(r => false, "key22", Arg.Text("arg221", "argValue221"), Arg.Text("arg22x", "argValue222")).WithPath("member2") .RuleTemplate(r => false, "key23", Arg.Text("arg23", "argValue23")).WithPath("member2") .RuleTemplate(r => false, "key3", Arg.Text("arg3", "argValue3")).WithPath("member3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "key1", }, Args = new[] { Arg.Text("arg1", "argValue1"), }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "key21", }, Args = new[] { Arg.Text("arg21", "argValue21"), }, }, new Error { Messages = new[] { "key22", }, Args = new[] { Arg.Text("arg221", "argValue221"), Arg.Text("arg222", "argValue222"), }, }, new Error { Messages = new[] { "key23", }, Args = new[] { Arg.Text("arg23", "argValue23"), }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "key3", }, Codes = new[] { "code3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) arg is missing: `arg222`"); } [Fact] public void Should_ReturnFailure_When_Error_Arg_InvalidType() { Specification specification = s => s .RuleTemplate(r => false, "key1", Arg.Text("arg1", "argValue1")).WithPath("member1") .RuleTemplate(r => false, "key21", Arg.Text("arg21", "argValue21")).WithPath("member2") .RuleTemplate(r => false, "key22", Arg.Text("arg221", "argValue221"), Arg.Text("arg222", "argValue222")).WithPath("member2") .RuleTemplate(r => false, "key23", Arg.Text("arg23", "argValue23")).WithPath("member2") .RuleTemplate(r => false, "key3", Arg.Text("arg3", "argValue3")).WithPath("member3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "key1", }, Args = new[] { Arg.Text("arg1", "argValue1"), }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "key21", }, Args = new[] { Arg.Text("arg21", "argValue21"), }, }, new Error { Messages = new[] { "key22", }, Args = new[] { Arg.Text("arg221", "argValue221"), Arg.Number("arg222", 222), }, }, new Error { Messages = new[] { "key23", }, Args = new[] { Arg.Text("arg23", "argValue23"), }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "key3", }, Codes = new[] { "code3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) arg (name `arg222`) type to be `Validot.Errors.Args.NumberArg`, but found `Validot.Errors.Args.TextArg`"); } [Fact] public void Should_ReturnFailure_When_Error_Arg_InvalidValue() { Specification specification = s => s .RuleTemplate(r => false, "key1", Arg.Text("arg1", "argValue1")).WithPath("member1") .RuleTemplate(r => false, "key21", Arg.Text("arg21", "argValue21")).WithPath("member2") .RuleTemplate(r => false, "key22", Arg.Text("arg221", "argValue221"), Arg.Text("arg222", "argValue22x")).WithPath("member2") .RuleTemplate(r => false, "key23", Arg.Text("arg23", "argValue23")).WithPath("member2") .RuleTemplate(r => false, "key3", Arg.Text("arg3", "argValue3")).WithPath("member3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "key1", }, Args = new[] { Arg.Text("arg1", "argValue1"), }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "key21", }, Args = new[] { Arg.Text("arg21", "argValue21"), }, }, new Error { Messages = new[] { "key22", }, Args = new[] { Arg.Text("arg221", "argValue221"), Arg.Text("arg222", "argValue222"), }, }, new Error { Messages = new[] { "key23", }, Args = new[] { Arg.Text("arg23", "argValue23"), }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "key3", }, Codes = new[] { "code3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) arg (name `arg222`) value to be `argValue222`, but found `argValue22x`"); } [Fact] public void Should_ReturnFailure_When_Error_Arg_InvalidValue_Double() { Specification specification = s => s .RuleTemplate(r => false, "key", Arg.Number("arg", 123.123457D)).WithPath("member"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member"] = new[] { new Error { Messages = new[] { "key", }, Args = new[] { Arg.Number("arg", 123.123456D), }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member`, index 0) arg (name `arg`) double value to be `123.123456`, but found `123.123457`"); } [Fact] public void Should_ReturnFailure_When_Error_Arg_InvalidValue_Float() { Specification specification = s => s .RuleTemplate(r => false, "key", Arg.Number("arg", 123.1235F)).WithPath("member"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member"] = new[] { new Error { Messages = new[] { "key", }, Args = new[] { Arg.Number("arg", 123.1234F), }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member`, index 0) arg (name `arg`) float value to be `123.1234`, but found `123.1235`"); } [Fact] public void Should_ReturnFailure_When_Error_Args_InvalidAmount() { Specification specification = s => s .RuleTemplate(r => false, "key1", Arg.Text("arg1", "argValue1")).WithPath("member1") .RuleTemplate(r => false, "key21", Arg.Text("arg21", "argValue21")).WithPath("member2") .RuleTemplate(r => false, "key22", Arg.Text("arg22", "argValue22")).WithPath("member2") .RuleTemplate(r => false, "key23", Arg.Text("arg23", "argValue23")).WithPath("member2") .RuleTemplate(r => false, "key3", Arg.Text("arg3", "argValue3")).WithPath("member3") ; TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "key1", }, Args = new[] { Arg.Text("arg1", "argValue1"), }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "key21", }, Args = new[] { Arg.Text("arg21", "argValue21"), }, }, new Error { Messages = new[] { "key22", }, Args = new[] { Arg.Text("arg221", "argValue221"), Arg.Text("arg222", "argValue222"), }, }, new Error { Messages = new[] { "key23", }, Args = new[] { Arg.Text("arg23", "argValue23"), }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "key3", }, Codes = new[] { "code3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) args amount to be 2, but found 1"); } [Fact] public void Should_ReturnFailure_When_Error_Args_Presence_ExistButNotExpected() { Specification specification = s => s .RuleTemplate(m => false, "message1", Arg.Text("arg", "argValue")).WithPath("member1"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "message1", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member1`, index 0) args amount to be 0, but found 1"); } [Fact] public void Should_ReturnFailure_When_Error_Args_Presence_NotExistButExpected() { Specification specification = s => s .RuleTemplate(m => false, "message1").WithPath("member1"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "message1", }, Args = new[] { Arg.GuidValue("arg", Guid.Empty), }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member1`, index 0) args amount to be 1, but found 0"); } [Fact] public void Should_ReturnFailure_When_Error_Code_InvalidContent() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithExtraCode("code1") .Rule(m => false).WithPath("member2") .WithExtraCode("code211") .WithExtraCode("code212") .WithExtraCode("code213") .Rule(m => false).WithPath("member2") .WithExtraCode("code221") .WithExtraCode("code22x") .WithExtraCode("code223") .Rule(m => false).WithPath("member2") .WithExtraCode("code231") .WithExtraCode("code232") .WithExtraCode("code233") .Rule(m => false).WithPath("member3").WithExtraCode("code3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Codes = new[] { "code1", }, }, }, ["member2"] = new[] { new Error { Codes = new[] { "code211", "code212", "code213", }, }, new Error { Codes = new[] { "code221", "code222", "code223", }, }, new Error { Codes = new[] { "code231", "code232", "code233", }, }, }, ["member3"] = new[] { new Error { Codes = new[] { "code3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) code (index 1) to be `code222`, but found `code22x`"); } [Fact] public void Should_ReturnFailure_When_Error_Codes_InvalidAmount() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithExtraCode("code1") .Rule(m => false).WithPath("member2") .WithExtraCode("code211") .WithExtraCode("code212") .WithExtraCode("code213") .Rule(m => false).WithPath("member2") .WithExtraCode("code221") .Rule(m => false).WithPath("member2") .WithExtraCode("code231") .WithExtraCode("code232") .WithExtraCode("code233") .Rule(m => false).WithPath("member3").WithExtraCode("code3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Codes = new[] { "code1", }, }, }, ["member2"] = new[] { new Error { Codes = new[] { "code211", "code212", "code213", }, }, new Error { Codes = new[] { "code221", "code222", "code223", }, }, new Error { Codes = new[] { "code231", "code232", "code233", }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "code3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) codes amount to be 3, but found 1"); } [Fact] public void Should_ReturnFailure_When_Error_Codes_Presence_ExistButNotExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("message").WithExtraCode("code1").WithExtraCode("code2"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "message", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member1`, index 0) codes amount to be 0, but found 2"); } [Fact] public void Should_ReturnFailure_When_Error_Codes_Presence_NotExistButExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("message1"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "message1", }, Codes = new[] { "code1", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member1`, index 0) codes amount to be 1, but found 0"); } [Fact] public void Should_ReturnFailure_When_Error_Message_InvalidContent() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("error1") .Rule(m => false).WithPath("member2") .WithExtraMessage("error211") .WithExtraMessage("error212") .WithExtraMessage("error213") .Rule(m => false).WithPath("member2") .WithExtraMessage("error221") .WithExtraMessage("error22x") .WithExtraMessage("error223") .Rule(m => false).WithPath("member2") .WithExtraMessage("error231") .WithExtraMessage("error232") .WithExtraMessage("error233") .Rule(m => false).WithPath("member3").WithMessage("error3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "error1", }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "error211", "error212", "error213", }, }, new Error { Messages = new[] { "error221", "error222", "error223", }, }, new Error { Messages = new[] { "error231", "error232", "error233", }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "error3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) message (index 1) to be `error222`, but found `error22x`"); } [Fact] public void Should_ReturnFailure_When_Error_Messages_InvalidAmount() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("error1") .Rule(m => false).WithPath("member2") .WithExtraMessage("error211") .WithExtraMessage("error212") .WithExtraMessage("error213") .Rule(m => false).WithPath("member2") .WithExtraMessage("error221") .Rule(m => false).WithPath("member2") .WithExtraMessage("error231") .WithExtraMessage("error232") .WithExtraMessage("error233") .Rule(m => false).WithPath("member3").WithMessage("error3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "error1", }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "error211", "error212", "error213", }, }, new Error { Messages = new[] { "error221", "error222", "error223", }, }, new Error { Messages = new[] { "error231", "error232", "error233", }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "error3", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member2`, index 1) messages amount to be 3, but found 1"); } [Fact] public void Should_ReturnFailure_When_Error_Messages_Presence_ExistButNotExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("error1").WithExtraMessage("extra1"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Codes = new[] { "code1", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member1`, index 0) messages amount to be 0, but found 2"); } [Fact] public void Should_ReturnFailure_When_Error_Messages_Presence_NotExistButExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithCode("code1"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "message1", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error (for path `member1`, index 0) messages amount to be 1, but found 0"); } [Fact] public void Should_ReturnFailure_When_Model_IsInvalid_And_NoErrorsAreExpected() { TestResult testResult = Tester.TestSpecification( new object(), s => s.Rule(m => false)); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected result IsValid: True, but AnyErrors: True"); } [Fact] public void Should_ReturnFailure_When_Model_IsValid_And_ErrorsAreExpected() { TestResult testResult = Tester.TestSpecification( new object(), s => s.Rule(m => true), new Dictionary> { ["member"] = new[] { new Error(), }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected result IsValid: False, but AnyErrors: False"); } [Fact] public void Should_ReturnFailure_When_Path_ExpectedPathIsMissing() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("error1") .Rule(m => false).WithPath("member_two").WithMessage("error2") .Rule(m => false).WithPath("member3").WithMessage("error3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error(), }, ["member2"] = new[] { new Error(), }, ["member3"] = new[] { new Error(), }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected error path is missing: `member2`"); } [Fact] public void Should_ReturnFailure_When_PathErrors_Amount_DifferentThanExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("error1") .Rule(m => false).WithPath("member2").WithMessage("error2") .Rule(m => false).WithPath("member3").WithMessage("error3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "error1", }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "error2", }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "error3", }, }, new Error { Messages = new[] { "error31", }, }, }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected errors amount (for path `member3`): 2, but found 1"); } [Fact] public void Should_ReturnFailure_When_Paths_Amount_DifferentThanExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1").WithMessage("error1") .Rule(m => false).WithPath("member2").WithMessage("error2"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error(), }, ["member2"] = new[] { new Error(), }, ["member3"] = new[] { new Error(), }, }); testResult.Success.Should().BeFalse(); testResult.Message.Should().Be("Expected amount of paths with errors: 3, but found: 2"); } [Fact] public void Should_ReturnSuccess_When_ModelIsValid_And_NoErrorsAreExpected() { TestResult testResult = Tester.TestSpecification(new object(), s => s.Rule(m => true)); testResult.Success.Should().BeTrue(); } [Fact] public void Should_ReturnSuccess_When_AllAsExpected() { Specification specification = s => s .Rule(m => false).WithPath("member1") .WithMessage("error1") .WithExtraMessage("error1_1") .WithExtraCode("code1_1") .RuleTemplate(m => false, "error21", Arg.Number("arg21", 12345.6789012345M)).WithPath("member2") .RuleTemplate(m => false, "error22", Arg.Text("arg221", "awesome"), Arg.Enum("arg222", StringComparison.InvariantCultureIgnoreCase)).WithPath("member2") .RuleTemplate(m => false, "error3", Arg.Type("arg3", typeof(Guid))).WithPath("member3").WithExtraMessage("extramessage3").WithExtraCode("extracode3"); TestResult testResult = Tester.TestSpecification( new object(), specification, new Dictionary> { ["member1"] = new[] { new Error { Messages = new[] { "error1", "error1_1", }, Codes = new[] { "code1_1", }, }, }, ["member2"] = new[] { new Error { Messages = new[] { "error21", }, Args = new[] { Arg.Number("arg21", 12345.6789012345M), }, }, new Error { Messages = new[] { "error22", }, Args = new[] { Arg.Text("arg221", "awesome"), Arg.Enum("arg222", StringComparison.InvariantCultureIgnoreCase), }, }, }, ["member3"] = new[] { new Error { Messages = new[] { "error3", "extramessage3", }, Codes = new[] { "extracode3", }, Args = new[] { Arg.Type("arg3", typeof(Guid)), }, }, }, }); testResult.Success.Should().BeTrue(); } } public class TestSingleRule { [Fact] public void Should_Pass_When_AsExpected_ModelIsValid() { var specification = new Specification(m => m.Rule(r => true)); Tester.TestSingleRule(new object(), specification, true); } [Fact] public void Should_Pass_When_AsExpected_ModelIsInvalid_WithMessage() { var specification = new Specification(m => m.Rule(r => false).WithMessage("message")); Tester.TestSingleRule(new object(), specification, false, "message"); } [Fact] public void Should_Pass_When_AsExpected_ModelIsInvalid_WithMessage_And_Args() { var specification = new Specification(m => m.RuleTemplate(r => false, "message", Arg.Number("arg1", 1), Arg.Text("arg2", "argValue2"))); Tester.TestSingleRule(new object(), specification, false, "message", Arg.Number("arg1", 1), Arg.Text("arg2", "argValue2")); } [Fact] public void Should_ThrowException_When_ArgsWithoutMessage() { var specification = new Specification(m => m); Action action = () => Tester.TestSingleRule(new object(), specification, true, null, Arg.Number("arg", 1)); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowTestFailedException_When_Errors_Expected_And_NotExist() { var specification = new Specification(m => m.Rule(r => true).WithMessage("message")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message"); action.Should().ThrowExactly().WithMessage($"Expected result IsValid: False, but AnyErrors: False"); } [Fact] public void Should_ThrowTestFailedException_When_Errors_NotExpected_And_Exist() { var specification = new Specification(m => m.Rule(r => false).WithMessage("message")); Action action = () => Tester.TestSingleRule(new object(), specification, true); action.Should().ThrowExactly().WithMessage($"Expected result IsValid: True, but AnyErrors: True"); } [Fact] public void Should_ThrowTestFailedException_When_Errors_MultipleErrors_UnderOnePath() { var specification = new Specification(m => m .Rule(r => false).WithMessage("message1") .Rule(r => false).WithMessage("message2")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message1"); action.Should().ThrowExactly().WithMessage($"Expected errors amount (for path ``): 1, but found 2"); } [Fact] public void Should_ThrowTestFailedException_When_Errors_MultipleErrors_UnderDifferentPaths() { var specification = new Specification(m => m .Rule(r => false).WithMessage("message1") .Rule(r => false).WithPath("member1").WithMessage("message2")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message1"); action.Should().ThrowExactly().WithMessage($"Expected amount of paths with errors: 1, but found: 2"); } [Fact] public void Should_ThrowTestFailedException_When_Error_NotInRoot() { var specification = new Specification(m => m.Rule(r => false).WithPath("member1").WithMessage("message1")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message1"); action.Should().ThrowExactly().WithMessage($"Expected error path is missing: ``"); } [Fact] public void Should_ThrowTestFailedException_When_MultipleMessages() { var specification = new Specification(m => m.Rule(r => false).WithMessage("message1").WithExtraMessage("message2")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message1"); action.Should().ThrowExactly().WithMessage($"Expected error (for path ``, index 0) messages amount to be 1, but found 2"); } [Fact] public void Should_ThrowTestFailedException_When_AnyCode() { var specification = new Specification(m => m.Rule(r => false).WithMessage("message1").WithExtraCode("message2")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message1"); action.Should().ThrowExactly().WithMessage($"Expected error (for path ``, index 0) codes amount to be 0, but found 1"); } [Fact] public void Should_ThrowTestFailedException_When_Error_HasArgs_And_ArgsNotExpected() { var specification = new Specification(m => m.RuleTemplate(r => false, "message", Arg.Text("arg", "argValue"))); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message"); action.Should().ThrowExactly().WithMessage($"Expected error (for path ``, index 0) args amount to be 0, but found 1"); } [Fact] public void Should_ThrowTestFailedException_When_Error_HasNoArgs_And_ArgsAreExpected() { var specification = new Specification(m => m.RuleTemplate(r => false, "message")); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message", Arg.Text("arg", "argValue")); action.Should().ThrowExactly().WithMessage($"Expected error (for path ``, index 0) args amount to be 1, but found 0"); } [Fact] public void Should_ThrowTestFailedException_When_Error_Args_InvalidAmount() { var specification = new Specification(m => m.RuleTemplate(r => false, "message", Arg.Text("arg1", "argValue1"), Arg.Text("arg2", "argValue2"))); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message", Arg.Text("arg", "argValue")); action.Should().ThrowExactly().WithMessage($"Expected error (for path ``, index 0) args amount to be 1, but found 2"); } [Fact] public void Should_ThrowTestFailedException_When_Error_Args_InvalidType() { var specification = new Specification(m => m.RuleTemplate(r => false, "message", Arg.Text("arg1", "argValue1"))); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message", Arg.Number("arg1", 1)); action.Should().ThrowExactly().WithMessage("Expected error (for path ``, index 0) arg (name `arg1`) type to be `Validot.Errors.Args.NumberArg`, but found `Validot.Errors.Args.TextArg`"); } [Fact] public void Should_ThrowTestFailedException_When_Error_Args_InvalidName() { var specification = new Specification(m => m.RuleTemplate(r => false, "message", Arg.Text("arg2", "argValue1"))); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message", Arg.Number("arg1", 1)); action.Should().ThrowExactly().WithMessage("Expected error (for path ``, index 0) arg is missing: `arg1`"); } [Fact] public void Should_ThrowTestFailedException_When_Error_Args_InvalidValue() { var specification = new Specification(m => m.RuleTemplate(r => false, "message", Arg.Text("arg1", "argValue1"), Arg.Text("arg2", "argValue2X"))); Action action = () => Tester.TestSingleRule(new object(), specification, false, "message", Arg.Text("arg1", "argValue1"), Arg.Text("arg2", "argValue2")); action.Should().ThrowExactly().WithMessage("Expected error (for path ``, index 0) arg (name `arg2`) value to be `argValue2`, but found `argValue2X`"); } } public class TestResultToString { [Fact] public void Should_ThrowException_When_NullString() { Action action = () => { Tester.TestResultToString(null, ToStringContentType.Messages, "asd"); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullExpectedLines() { Action action = () => { Tester.TestResultToString("abc", ToStringContentType.Messages, null); }; action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_EmptyExpectedLines() { Action action = () => { Tester.TestResultToString("abc", ToStringContentType.Messages, new string[] { }); }; action.Should().ThrowExactly().And.Message.Should().StartWith("Empty list of expected lines"); } [Theory] [InlineData(2)] [InlineData(3)] [InlineData(10)] public void Should_ThrowException_When_CodesExpected_And_MoreThanOneLine(int lines) { Action action = () => { Tester.TestResultToString( "abc", ToStringContentType.Codes, Enumerable.Range(0, lines).Select(i => $"{i}").ToArray() ); }; action.Should().ThrowExactly().And.Message.Should().StartWith($"Expected codes only (all in the single line), but found lines: {lines}"); } [Theory] [InlineData(1)] [InlineData(2)] public void Should_ThrowException_When_MessagesAndCodesExpected_And_LessThanThreeLines(int lines) { Action action = () => { Tester.TestResultToString( "abc", ToStringContentType.MessagesAndCodes, Enumerable.Range(0, lines).Select(i => $"{i}").ToArray() ); }; action.Should().ThrowExactly().And.Message.Should().StartWith($"Expected codes and messages (so at least 3 lines), but found lines: {lines}"); } [Theory] [InlineData(3)] [InlineData(5)] [InlineData(10)] public void Should_ThrowException_When_MessagesAndCodesExpected_And_SecondLineNotEmpty(int lines) { var expectedLines = Enumerable.Range(0, lines).Select(i => $"{i}").ToArray(); Action action = () => { Tester.TestResultToString( "abc", ToStringContentType.MessagesAndCodes, expectedLines ); }; action.Should().ThrowExactly().And.Message.Should().StartWith($"Expected codes and messages (divided by a single empty line), but found in second line: {expectedLines[1]}"); } [Theory] [InlineData(3)] [InlineData(5)] [InlineData(10)] public void Should_ThrowException_When_MessagesAndCodesExpected_And_ExtraEmptyLine(int lines) { var expectedLines = Enumerable.Range(0, lines).Select(i => $"{i}").ToArray(); expectedLines[1] = ""; expectedLines[lines - 1] = ""; Action action = () => { Tester.TestResultToString( "abc", ToStringContentType.MessagesAndCodes, expectedLines); }; action.Should().ThrowExactly().And.Message.Should().StartWith("Expected codes and messages (divided by a single empty line), also another empty line"); } [Theory] [InlineData(1)] [InlineData(3)] [InlineData(5)] [InlineData(10)] public void Should_ThrowException_When_MessagesExpected_And_EmptyLine(int lines) { var expectedLines = Enumerable.Range(0, lines).Select(i => $"{i}").ToArray(); expectedLines[lines - 1] = ""; Action action = () => { Tester.TestResultToString( "abc", ToStringContentType.Messages, expectedLines); }; action.Should().ThrowExactly().And.Message.Should().StartWith($"Expected messages only, but found empty line"); } [Theory] [InlineData(1, 3, ToStringContentType.Messages)] [InlineData(5, 1, ToStringContentType.Messages)] [InlineData(5, 3, ToStringContentType.MessagesAndCodes)] [InlineData(8, 9, ToStringContentType.MessagesAndCodes)] public void Should_Fail_When_DifferentLineAmount(int linesCount, int expectedLinesCount, ToStringContentType toStringContentType) { var lines = Enumerable.Range(0, linesCount).Select(i => $"{i}").ToArray(); var expectedLines = Enumerable.Range(0, expectedLinesCount).Select(i => $"{i}").ToArray(); if (toStringContentType == ToStringContentType.MessagesAndCodes) { expectedLines[1] = ""; lines[1] = ""; } var input = string.Join(Environment.NewLine, lines); var result = Tester.TestResultToString(input, toStringContentType, expectedLines); result.Success.Should().BeFalse(); result.Message.Should().Be($"Expected amount of lines: {expectedLinesCount}, but found: {lines.Length}"); } public static IEnumerable Should_Fail_When_MissingCodes_Data() { foreach (var expectedStringContent in new[] { ToStringContentType.Codes, ToStringContentType.MessagesAndCodes }) { yield return new object[] { "a, b, c, d", "a, b, c, d, e, f, g", "e, f, g", expectedStringContent }; yield return new object[] { "a, b, c, d, e", "a, b, c, d, e, f, g", "f, g", expectedStringContent }; yield return new object[] { "a, b, c, d, e, f", "a, b, c, d, e, f, g", "g", expectedStringContent }; yield return new object[] { "a, c, e, f", "a, b, c, d, e, f, g", "b, d, g", expectedStringContent }; yield return new object[] { "f", "a, b, c, d, e, f, g", "a, b, c, d, e, g", expectedStringContent }; yield return new object[] { "e, c, a", "a, b, c, d, e, f, g", "b, d, f, g", expectedStringContent }; } } [Theory] [MemberData(nameof(Should_Fail_When_MissingCodes_Data))] public void Should_Fail_When_MissingCodes(string codesString, string expectedCodesString, string missingCodesString, ToStringContentType toStringContentType) { if (toStringContentType == ToStringContentType.MessagesAndCodes) { codesString += string.Join(Environment.NewLine, new[] { Environment.NewLine, "m1", "m2", "m3" }); expectedCodesString += string.Join(Environment.NewLine, new[] { Environment.NewLine, "m1", "m2", "m3" }); } var result = Tester.TestResultToString(codesString, toStringContentType, expectedCodesString.Split(new[] { Environment.NewLine }, StringSplitOptions.None)); result.Success.Should().BeFalse(); result.Message.Should().Be($"Expected codes that are missing: {missingCodesString}"); } public static IEnumerable Should_Fail_When_InvalidAmountOfCodes_Data() { foreach (var expectedStringContent in new[] { ToStringContentType.Codes, ToStringContentType.MessagesAndCodes }) { yield return new object[] { "a, b, c, d, e, f, g, A, B", "a, b, c, d, e, f, g", expectedStringContent }; yield return new object[] { "a, b, c, d, e, f, g", "a, b, c, d, e, f", expectedStringContent }; yield return new object[] { "a, b, c, d, e, f, g", "a, c, e, f", expectedStringContent }; yield return new object[] { "a, b, c, d, e, f, g", "a, b, c, d, e, g", expectedStringContent }; yield return new object[] { "a, b, c, d, e, f, g, g, a", "a, b, c, d, e, f, g", expectedStringContent }; } } [Theory] [MemberData(nameof(Should_Fail_When_InvalidAmountOfCodes_Data))] public void Should_Fail_When_InvalidAmountOfCodes(string codesString, string expectedCodesString, ToStringContentType toStringContentType) { var codeAmount = codesString.Split(new[] { ", " }, StringSplitOptions.None).Length; var expectedCodeAmount = expectedCodesString.Split(new[] { ", " }, StringSplitOptions.None).Length; if (toStringContentType == ToStringContentType.MessagesAndCodes) { codesString += string.Join(Environment.NewLine, new[] { Environment.NewLine, "m1", "m2", "m3" }); expectedCodesString += string.Join(Environment.NewLine, new[] { Environment.NewLine, "m1", "m2", "m3" }); } var result = Tester.TestResultToString(codesString, toStringContentType, expectedCodesString.Split(new[] { Environment.NewLine }, StringSplitOptions.None)); result.Success.Should().BeFalse(); result.Message.Should().Be($"Expected amount of codes: {expectedCodeAmount}, but found: {codeAmount}"); } [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public void Should_Fail_When_ExpectingMessagesAndCodes_And_SecondLineIsNotEmpty(int linesCount) { var lines = Enumerable.Range(0, linesCount + 2).Select(i => $"{i}").ToArray(); lines[0] = "a, b, c"; lines[1] = "_not_empty_"; var expectedLines = Enumerable.Range(0, linesCount + 2).Select(i => $"{i}").ToArray(); expectedLines[0] = "a, b, c"; expectedLines[1] = string.Empty; var input = string.Join(Environment.NewLine, lines); var result = Tester.TestResultToString(input, ToStringContentType.MessagesAndCodes, expectedLines); result.Success.Should().BeFalse(); result.Message.Should().Be($"Expected codes and messages (divided by a single line), but found in second line: _not_empty_"); } public static IEnumerable Should_Fail_When_MissingMessages_Data() { foreach (var expectedStringContent in new[] { ToStringContentType.Messages, ToStringContentType.MessagesAndCodes }) { yield return new object[] { new[] { "a", "b", "c", "X" }, new[] { "a", "b", "c", "e" }, "`e`", expectedStringContent }; yield return new object[] { new[] { "c", "a", "X", "b" }, new[] { "a", "b", "c", "e" }, "`e`", expectedStringContent }; yield return new object[] { new[] { "c", "X", "a", "b", }, new[] { "a", "b", "c", "e" }, "`e`", expectedStringContent }; yield return new object[] { new[] { "a", "b", "c", "X", "Y" }, new[] { "a", "b", "c", "e", "f" }, "`e`, `f`", expectedStringContent }; yield return new object[] { new[] { "X", "c", "a", "Y", "b" }, new[] { "a", "b", "c", "e", "f" }, "`e`, `f`", expectedStringContent }; yield return new object[] { new[] { "a", "X", "Y", "X", "Y" }, new[] { "a", "b", "c", "e", "f" }, "`b`, `c`, `e`, `f`", expectedStringContent }; } } [Theory] [MemberData(nameof(Should_Fail_When_MissingMessages_Data))] public void Should_Fail_When_MissingMessages(string[] messages, string[] expectedMessages, string missingMessages, ToStringContentType toStringContentType) { if (toStringContentType == ToStringContentType.MessagesAndCodes) { var codesLines = new[] { "a, b, c", "" }; messages = codesLines.Concat(messages).ToArray(); expectedMessages = codesLines.Concat(expectedMessages).ToArray(); } var input = string.Join(Environment.NewLine, messages); var result = Tester.TestResultToString(input, toStringContentType, expectedMessages); result.Success.Should().BeFalse(); result.Message.Should().Be($"Expected messages that are missing: {missingMessages}"); } public static IEnumerable Should_Succeed_Data() { yield return new object[] { new[] { "a, b, c, d, e", "", "M1", "M2", "M3" }, new[] { "a, b, c, d, e", "", "M1", "M2", "M3" }, ToStringContentType.MessagesAndCodes }; yield return new object[] { new[] { "e, d, c, b, a", "", "M3", "M2", "M1" }, new[] { "a, b, c, d, e", "", "M1", "M2", "M3" }, ToStringContentType.MessagesAndCodes }; yield return new object[] { new[] { "c, d, e, a, b", "", "M1" }, new[] { "a, b, c, d, e", "", "M1" }, ToStringContentType.MessagesAndCodes }; yield return new object[] { new[] { "M1", "M2", "M3" }, new[] { "M1", "M2", "M3" }, ToStringContentType.Messages }; yield return new object[] { new[] { "M3", "M2", "M1" }, new[] { "M1", "M2", "M3" }, ToStringContentType.Messages }; yield return new object[] { new[] { "c, d, e, a, b", }, new[] { "a, b, c, d, e" }, ToStringContentType.Codes }; yield return new object[] { new[] { "a, b, c, d, e" }, new[] { "a, b, c, d, e" }, ToStringContentType.Codes }; } [Theory] [MemberData(nameof(Should_Succeed_Data))] public void Should_Succeed(string[] messages, string[] expectedMessages, ToStringContentType toStringContentType) { var input = string.Join(Environment.NewLine, messages); var result = Tester.TestResultToString(input, toStringContentType, expectedMessages); result.Success.Should().BeTrue(); result.Message.Should().BeNullOrEmpty(); } } public class ShouldResultToStringHaveLines { [Fact] public void Should_ThrowException_When_InvalidCodes() { Action action = () => { "a, b, c, d".ShouldResultToStringHaveLines( ToStringContentType.Codes, "b, c, a, d, e" ); }; action.Should().ThrowExactly().And.Message.Should().Be("Expected codes that are missing: e"); } [Fact] public void Should_ThrowException_When_InvalidCodes_WithMessages() { var messages = new[] { "a, b, c, d", "", "A", "B", "C", "D" }; Action action = () => { string.Join(Environment.NewLine, messages).ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "a, d, b, c, e", "", "C", "B", "A", "D" ); }; action.Should().ThrowExactly().And.Message.Should().Be("Expected codes that are missing: e"); } [Fact] public void Should_ThrowException_When_InvalidMessages() { var messages = new[] { "A", "B", "C", "X" }; Action action = () => { string.Join(Environment.NewLine, messages).ShouldResultToStringHaveLines( ToStringContentType.Messages, "C", "B", "A", "D" ); }; action.Should().ThrowExactly().And.Message.Should().Be("Expected messages that are missing: `D`"); } [Fact] public void Should_ThrowException_When_InvalidMessages_WithCodes() { var messages = new[] { "a, b, c, d, e", "", "A", "B", "C", "X" }; Action action = () => { string.Join(Environment.NewLine, messages).ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "a, b, c, d, e", "", "C", "B", "A", "D" ); }; action.Should().ThrowExactly().And.Message.Should().Be("Expected messages that are missing: `D`"); } [Fact] public void Should_NotThrowException_When_AllGood() { var messages = new[] { "a, b, c, d, e", "", "A", "B", "C", "D" }; Action action = () => { string.Join(Environment.NewLine, messages).ShouldResultToStringHaveLines( ToStringContentType.MessagesAndCodes, "a, b, c, d, e", "", "A", "B", "C", "D" ); }; action.Should().NotThrow(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/Chinese/ChineseTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.Chinese { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class ChineseTranslationsExtensionsTests { [Fact] public void Chinese_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.Chinese.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void Chinese_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.Chinese.Keys); } [Fact] public void Chinese_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.Chinese.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithChineseTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithChineseTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "Chinese", Translation.Chinese); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/English/EnglishTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.English { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class EnglishTranslationsExtensionsTests { [Fact] public void English_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.English.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void English_Should_HaveValues_ForAllMessageKeys() { Translation.English.Keys.Should().Contain(MessageKey.All); Translation.English.Keys.Should().HaveCount(MessageKey.All.Count); } [Fact] public void English_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.English.Keys); } [Fact] public void English_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.English.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithEnglishTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithEnglishTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "English", Translation.English); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/German/GermanTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.German { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class GermanTranslationsExtensionsTests { [Fact] public void German_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.German.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void German_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.German.Keys); } [Fact] public void German_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.German.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithGermanTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithGermanTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "German", Translation.German); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/MessageKeyTests.cs ================================================ namespace Validot.Tests.Unit.Translations { using System.Reflection; using FluentAssertions; using Validot.Translations; using Xunit; public class MessageKeyTests { [Fact] public void Should_HaveAllValuesAsThePathToTheProperty() { var globalType = typeof(MessageKey); var innerTypes = globalType.GetNestedTypes(BindingFlags.Public | BindingFlags.Static); foreach (var innerType in innerTypes) { var properties = innerType.GetProperties(BindingFlags.Public | BindingFlags.Static); foreach (var property in properties) { var value = property.GetValue(null); value.Should().BeOfType(); value.Should().Be($"{innerType.Name}.{property.Name}"); } } } [Fact] public void All_Should_ContainsAllPaths() { var counter = 0; var globalType = typeof(MessageKey); var innerTypes = globalType.GetNestedTypes(BindingFlags.Public | BindingFlags.Static); foreach (var innerType in innerTypes) { var properties = innerType.GetProperties(BindingFlags.Public | BindingFlags.Static); foreach (var property in properties) { var value = property.GetValue(null); value.Should().BeOfType(); counter++; MessageKey.All.Should().Contain(value as string); } } MessageKey.All.Should().NotContainNulls(); MessageKey.All.Should().HaveCount(counter); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/Polish/PolishTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.Polish { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class PolishTranslationsExtensionsTests { [Fact] public void Polish_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.Polish.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void Polish_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.Polish.Keys); } [Fact] public void Polish_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.Polish.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithPolishTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithPolishTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "Polish", Translation.Polish); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/Portuguese/PortugueseTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.Portuguese { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class PortugueseTranslationsExtensionsTests { [Fact] public void Portuguese_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.Portuguese.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void Portuguese_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.Portuguese.Keys); } [Fact] public void Portuguese_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.Portuguese.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithPortugueseTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithPortugueseTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "Portuguese", Translation.Portuguese); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/Russian/RussianTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.Russian { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class RussianTranslationsExtensionsTests { [Fact] public void Russian_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.Russian.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void Russian_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.Russian.Keys); } [Fact] public void Russian_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.Russian.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithRussianTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithRussianTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "Russian", Translation.Russian); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/Spanish/SpanishTranslationsExtensionsTests.cs ================================================ namespace Validot.Tests.Unit.Translations.Spanish { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class SpanishTranslationsExtensionsTests { [Fact] public void Spanish_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation.Spanish.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void Spanish_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation.Spanish.Keys); } [Fact] public void Spanish_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation.Spanish.ShouldContainOnlyValidPlaceholders(); } [Fact] public void WithSpanishTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.WithSpanishTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "Spanish", Translation.Spanish); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/TranslationCompilerTests.cs ================================================ namespace Validot.Tests.Unit.Translations { using System; using FluentAssertions; using Validot.Translations; using Xunit; public class TranslationCompilerTests { [Fact] public void Should_Initialize() { _ = new TranslationCompiler(); } [Fact] public void Should_Initialize_Empty() { var translationCompiler = new TranslationCompiler(); translationCompiler.Translations.Should().NotBeNull(); translationCompiler.Translations.Should().BeEmpty(); } [Theory] [InlineData(null, "2", "3")] [InlineData("1", null, "3")] [InlineData("1", "2", null)] [InlineData("1", null, null)] [InlineData(null, "2", null)] [InlineData(null, null, "3")] [InlineData(null, null, null)] public void Add_Should_ThrowException_When_NullArgs(string name, string messageKey, string tralsnation) { var translationCompiler = new TranslationCompiler(); Action action = () => translationCompiler.Add(name, messageKey, tralsnation); action.Should().Throw(); } [Fact] public void Add_Should_AddTranslation() { var translationCompiler = new TranslationCompiler(); translationCompiler.Add("name", "key", "value"); translationCompiler.Translations.Keys.Should().HaveCount(1); translationCompiler.Translations.Keys.Should().Contain("name"); translationCompiler.Translations["name"].Keys.Should().HaveCount(1); translationCompiler.Translations["name"].Keys.Should().Contain("key"); translationCompiler.Translations["name"]["key"].Should().Be("value"); } [Fact] public void Add_Should_AddTranslation_ManyTimes() { var translationCompiler = new TranslationCompiler(); translationCompiler.Add("name1", "key1", "value1"); translationCompiler.Add("name2", "key2", "value2"); translationCompiler.Translations.Keys.Should().HaveCount(2); translationCompiler.Translations.Keys.Should().Contain("name1"); translationCompiler.Translations.Keys.Should().Contain("name2"); translationCompiler.Translations["name1"].Keys.Should().HaveCount(1); translationCompiler.Translations["name1"].Keys.Should().Contain("key1"); translationCompiler.Translations["name1"]["key1"].Should().Be("value1"); translationCompiler.Translations["name2"].Keys.Should().HaveCount(1); translationCompiler.Translations["name2"].Keys.Should().Contain("key2"); translationCompiler.Translations["name2"]["key2"].Should().Be("value2"); } [Fact] public void Add_Should_AddTranslation_ManyTimes_SameTranslation() { var translationCompiler = new TranslationCompiler(); translationCompiler.Add("name", "key1", "value1"); translationCompiler.Add("name", "key2", "value2"); translationCompiler.Translations.Keys.Should().HaveCount(1); translationCompiler.Translations.Keys.Should().Contain("name"); translationCompiler.Translations["name"].Keys.Should().HaveCount(2); translationCompiler.Translations["name"].Keys.Should().Contain("key1"); translationCompiler.Translations["name"]["key1"].Should().Be("value1"); translationCompiler.Translations["name"].Keys.Should().Contain("key2"); translationCompiler.Translations["name"]["key2"].Should().Be("value2"); } [Fact] public void Add_Should_AddTranslation_Should_OverwriteValue() { var translationCompiler = new TranslationCompiler(); translationCompiler.Add("name1", "key1", "value1"); translationCompiler.Add("name2", "key2", "value2"); translationCompiler.Add("name2", "key2", "VALUE_2"); translationCompiler.Translations.Keys.Should().HaveCount(2); translationCompiler.Translations.Keys.Should().Contain("name1"); translationCompiler.Translations.Keys.Should().Contain("name2"); translationCompiler.Translations["name1"].Keys.Should().HaveCount(1); translationCompiler.Translations["name1"].Keys.Should().Contain("key1"); translationCompiler.Translations["name1"]["key1"].Should().Be("value1"); translationCompiler.Translations["name2"].Keys.Should().HaveCount(1); translationCompiler.Translations["name2"].Keys.Should().Contain("key2"); translationCompiler.Translations["name2"]["key2"].Should().Be("VALUE_2"); } [Fact] public void Add_Should_AddTranslation_Should_OverwriteValue_WhenSameKeysInDifferentDictionaries() { var translationCompiler = new TranslationCompiler(); translationCompiler.Add("name1", "key", "value1"); translationCompiler.Add("name2", "key", "value2"); translationCompiler.Add("name2", "key", "VALUE_2"); translationCompiler.Translations.Keys.Should().HaveCount(2); translationCompiler.Translations.Keys.Should().Contain("name1"); translationCompiler.Translations.Keys.Should().Contain("name2"); translationCompiler.Translations["name1"].Keys.Should().HaveCount(1); translationCompiler.Translations["name1"].Keys.Should().Contain("key"); translationCompiler.Translations["name1"]["key"].Should().Be("value1"); translationCompiler.Translations["name2"].Keys.Should().HaveCount(1); translationCompiler.Translations["name2"].Keys.Should().Contain("key"); translationCompiler.Translations["name2"]["key"].Should().Be("VALUE_2"); } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/TranslationTestHelpers.cs ================================================ namespace Validot.Tests.Unit.Translations { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Errors.Args; using Validot.Translations; public static class TranslationTestHelpers { public static void ShouldBeLikeTranslations(this IReadOnlyDictionary> @this, IReadOnlyDictionary> baseDictionary) { @this.Should().NotBeNull(); baseDictionary.Should().NotBeNull(); @this.Should().NotBeSameAs(baseDictionary); @this.Keys.Should().HaveCount(baseDictionary.Count); foreach (var baseKey in baseDictionary.Keys) { @this.Keys.Should().Contain(baseKey); @this[baseKey].Should().NotBeSameAs(baseDictionary[baseKey]); @this[baseKey].Keys.Should().HaveCount(baseDictionary[baseKey].Count); foreach (var baseEntryKey in baseDictionary[baseKey].Keys) { @this[baseKey].Keys.Should().Contain(baseEntryKey); @this[baseKey][baseEntryKey].Should().Be(baseDictionary[baseKey][baseEntryKey]); } } } public static void ShouldContainSingleTranslation(IReadOnlyDictionary> settingsTranslations, string translationName, IReadOnlyDictionary translation) { settingsTranslations.Should().NotBeEmpty(); settingsTranslations.Should().HaveCount(1); settingsTranslations.Keys.Should().ContainSingle(translationName); var selectedTranslation = settingsTranslations[translationName]; selectedTranslation.Should().HaveCount(translation.Count); foreach (var pair in translation) { selectedTranslation.Keys.Should().Contain(pair.Key); selectedTranslation[pair.Key].Should().Be(pair.Value); } } public static void ShouldContainOnlyValidPlaceholders(this IReadOnlyDictionary translation) { TestPlaceholders(translation, MessageKey.Global.Error); TestPlaceholders(translation, MessageKey.Global.Required); TestPlaceholders(translation, MessageKey.Global.Forbidden); TestPlaceholders(translation, MessageKey.Global.ReferenceLoop); TestPlaceholders(translation, MessageKey.CharType.EqualToIgnoreCase, "value"); TestPlaceholders(translation, MessageKey.CharType.NotEqualToIgnoreCase, "value"); TestPlaceholders(translation, MessageKey.GuidType.EqualTo, "value"); TestPlaceholders(translation, MessageKey.GuidType.NotEqualTo, "value"); TestPlaceholders(translation, MessageKey.GuidType.NotEmpty, "value"); TestPlaceholders(translation, MessageKey.Collections.EmptyCollection); TestPlaceholders(translation, MessageKey.Collections.NotEmptyCollection); TestPlaceholders(translation, MessageKey.Collections.ExactCollectionSize, "size"); TestPlaceholders(translation, MessageKey.Collections.MaxCollectionSize, "max"); TestPlaceholders(translation, MessageKey.Collections.MinCollectionSize, "min"); TestPlaceholders(translation, MessageKey.Collections.CollectionSizeBetween, "min", "max"); TestPlaceholders(translation, MessageKey.Numbers.EqualTo, "value"); TestPlaceholders(translation, MessageKey.Numbers.NotEqualTo, "value"); TestPlaceholders(translation, MessageKey.Numbers.GreaterThan, "min"); TestPlaceholders(translation, MessageKey.Numbers.GreaterThanOrEqualTo, "min"); TestPlaceholders(translation, MessageKey.Numbers.LessThan, "max"); TestPlaceholders(translation, MessageKey.Numbers.LessThanOrEqualTo, "max"); TestPlaceholders(translation, MessageKey.Numbers.Between, "min", "max"); TestPlaceholders(translation, MessageKey.Numbers.BetweenOrEqualTo, "min", "max"); TestPlaceholders(translation, MessageKey.Numbers.NonZero); TestPlaceholders(translation, MessageKey.Numbers.Positive); TestPlaceholders(translation, MessageKey.Numbers.NonPositive); TestPlaceholders(translation, MessageKey.Numbers.Negative); TestPlaceholders(translation, MessageKey.Numbers.NonNegative); TestPlaceholders(translation, MessageKey.Numbers.NonNaN); TestPlaceholders(translation, MessageKey.Texts.Email, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Texts.EqualTo, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Texts.NotEqualTo, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Texts.Contains, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Texts.NotContains, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Texts.NotEmpty); TestPlaceholders(translation, MessageKey.Texts.NotWhiteSpace); TestPlaceholders(translation, MessageKey.Texts.SingleLine); TestPlaceholders(translation, MessageKey.Texts.ExactLength, "length"); TestPlaceholders(translation, MessageKey.Texts.MaxLength, "max"); TestPlaceholders(translation, MessageKey.Texts.MinLength, "min"); TestPlaceholders(translation, MessageKey.Texts.LengthBetween, "min", "max"); TestPlaceholders(translation, MessageKey.Texts.Matches, "pattern"); TestPlaceholders(translation, MessageKey.Texts.StartsWith, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Texts.EndsWith, "value", "stringComparison"); TestPlaceholders(translation, MessageKey.Times.EqualTo, "value", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.NotEqualTo, "value", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.After, "min", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.AfterOrEqualTo, "min", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.Before, "max", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.BeforeOrEqualTo, "max", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.Between, "min", "max", "timeComparison"); TestPlaceholders(translation, MessageKey.Times.BetweenOrEqualTo, "min", "max", "timeComparison"); TestPlaceholders(translation, MessageKey.TimeSpanType.EqualTo, "value"); TestPlaceholders(translation, MessageKey.TimeSpanType.NotEqualTo, "value"); TestPlaceholders(translation, MessageKey.TimeSpanType.GreaterThan, "min"); TestPlaceholders(translation, MessageKey.TimeSpanType.GreaterThanOrEqualTo, "min"); TestPlaceholders(translation, MessageKey.TimeSpanType.LessThan, "max"); TestPlaceholders(translation, MessageKey.TimeSpanType.LessThanOrEqualTo, "max"); TestPlaceholders(translation, MessageKey.TimeSpanType.Between, "min", "max"); TestPlaceholders(translation, MessageKey.TimeSpanType.BetweenOrEqualTo, "min", "max"); TestPlaceholders(translation, MessageKey.TimeSpanType.NonZero); TestPlaceholders(translation, MessageKey.TimeSpanType.Positive); TestPlaceholders(translation, MessageKey.TimeSpanType.NonPositive); TestPlaceholders(translation, MessageKey.TimeSpanType.Negative); TestPlaceholders(translation, MessageKey.TimeSpanType.NonNegative); } private static void TestPlaceholders(IReadOnlyDictionary translation, string key, params string[] allowedRulePlaceholders) { if (!translation.ContainsKey(key)) { return; } var message = translation[key]; var placeholders = ArgHelper.ExtractPlaceholders(message); var globalPlaceholders = new[] { "_name", "_translation" }; foreach (var placehodler in placeholders) { var placeholderIsAllowed = allowedRulePlaceholders.Concat(globalPlaceholders).Any(p => string.Equals(placehodler.Name, p, StringComparison.Ordinal)); placeholderIsAllowed.Should().BeTrue($"Placeholder `{placehodler.Name}` is not allowed in message `{message}`"); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Translations/_Template/_TemplateTranslationsExtensionsTests.cs.txt ================================================ namespace Validot.Tests.Unit.Translations._Template { using System.Linq; using FluentAssertions; using Validot.Settings; using Validot.Translations; using Xunit; public class _TemplateTranslationsExtensionsTests { [Fact] public void _Template_Should_HaveValues_NonNullNorEmptyNorWhiteSpace() { Translation._Template.Values.All(m => !string.IsNullOrWhiteSpace(m)).Should().BeTrue(); } [Fact] public void _Template_Should_HaveValues_OnlyFromMessageKeys() { MessageKey.All.Should().Contain(Translation._Template.Keys); } [Fact] public void _Template_Should_HaveValues_OnlyWithAllowedPlaceholders() { Translation._Template.ShouldContainOnlyValidPlaceholders(); } [Fact] public void With_TemplateTranslation_Should_AddTranslation() { var settings = new ValidatorSettings(); settings.With_TemplateTranslation(); TranslationTestHelpers.ShouldContainSingleTranslation(settings.Translations, "_Template", Translation._Template); } } } ================================================ FILE: tests/Validot.Tests.Unit/TypeStringifierTests.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using FluentAssertions; using Xunit; public class TypeStringifierTests { [Theory] [InlineData(typeof(int), "Int32")] [InlineData(typeof(int?), "Nullable")] [InlineData(typeof(IEnumerable), "IEnumerable")] [InlineData(typeof(Dictionary), "Dictionary")] [InlineData(typeof(Dictionary>), "Dictionary>>")] public void Should_Stringify_WithoutNamespaces(Type type, string expectedName) { type.GetFriendlyName().Should().Be(expectedName); } [Theory] [InlineData(typeof(int), "System.Int32")] [InlineData(typeof(int?), "System.Nullable")] [InlineData(typeof(IEnumerable), "System.Collections.Generic.IEnumerable")] [InlineData(typeof(Dictionary), "System.Collections.Generic.Dictionary")] [InlineData(typeof(Dictionary>), "System.Collections.Generic.Dictionary>>")] public void Should_Stringify_WithNamespaces(Type type, string expectedName) { type.GetFriendlyName(true).Should().Be(expectedName); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/DiscoveryContextTests.cs ================================================ namespace Validot.Tests.Unit.Validation { using System; using System.Globalization; using System.Linq; using FluentAssertions; using NSubstitute; using Validot.Errors; using Validot.Errors.Args; using Validot.Validation; using Validot.Validation.Scopes; using Xunit; public class DiscoveryContextTests { [Fact] public void Should_Initialize() { var actions = Substitute.For(); _ = new DiscoveryContext(actions, 0); } [Fact] public void Should_Initialize_WithDefaultValues() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.Errors.Should().BeEmpty(); context.Paths.Should().BeEmpty(); context.ReferenceLoopRoots.Should().BeEmpty(); } public class AddError { [Fact] public void Should_AddError_To_DefaultPath() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors[string.Empty].Should().HaveCount(1); context.Errors[string.Empty].ElementAt(0).Should().Be(123); } [Fact] public void Should_AddErrors_To_DefaultPath() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.AddError(123); context.AddError(1234); context.AddError(12345); context.Errors.Should().HaveCount(1); context.Errors[string.Empty].Should().HaveCount(3); context.Errors[string.Empty].ElementAt(0).Should().Be(123); context.Errors[string.Empty].ElementAt(1).Should().Be(1234); context.Errors[string.Empty].ElementAt(2).Should().Be(12345); } [Theory] [InlineData("path")] [InlineData("some.nested.path")] public void Should_AddError(string name) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(name); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors[name].Should().HaveCount(1); context.Errors[name].ElementAt(0).Should().Be(123); } [Theory] [InlineData("path")] [InlineData("some.nested.path")] public void Should_AddErrors(string name) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(name); context.AddError(123); context.AddError(1234); context.AddError(12345); context.Errors.Should().HaveCount(1); context.Errors[name].Should().HaveCount(3); context.Errors[name].ElementAt(0).Should().Be(123); context.Errors[name].ElementAt(1).Should().Be(1234); context.Errors[name].ElementAt(2).Should().Be(12345); } [Theory] [InlineData("")] [InlineData("path")] [InlineData("some.nested.path")] public void Should_AddError_When_AlreadyExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_False(string name) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(name); context.AddError(123); context.AddError(123); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors[name].Should().HaveCount(3); context.Errors[name].ElementAt(0).Should().Be(123); context.Errors[name].ElementAt(1).Should().Be(123); context.Errors[name].ElementAt(2).Should().Be(123); } [Theory] [InlineData("")] [InlineData("path")] [InlineData("some.nested.path")] public void Should_NotAddError_When_AlreadyExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_True(string name) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(name); context.AddError(123, true); context.AddError(123, true); context.AddError(123, true); context.Errors.Should().HaveCount(1); context.Errors[name].Should().HaveCount(1); context.Errors[name].ElementAt(0).Should().Be(123); } [Theory] [InlineData("")] [InlineData("path")] [InlineData("some.nested.path")] public void Should_AddError_When_NotExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_True(string name) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(name); context.AddError(123, true); context.AddError(1234, true); context.AddError(12345, true); context.Errors.Should().HaveCount(1); context.Errors[name].Should().HaveCount(3); context.Errors[name].ElementAt(0).Should().Be(123); context.Errors[name].ElementAt(1).Should().Be(1234); context.Errors[name].ElementAt(2).Should().Be(12345); } [Fact] public void Should_AddError_OnlyWhen_NotExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_True() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath("test1"); context.AddError(123, true); context.AddError(123, true); context.AddError(123); context.EnterPath("test2"); context.AddError(123, true); context.AddError(123, true); context.AddError(123); context.EnterPath("test3"); context.AddError(123, true); context.AddError(123); context.AddError(123); context.Errors.Should().HaveCount(3); context.Errors["test1"].Should().HaveCount(2); context.Errors["test1"].ElementAt(0).Should().Be(123); context.Errors["test1"].ElementAt(1).Should().Be(123); context.Errors["test1.test2"].Should().HaveCount(2); context.Errors["test1.test2"].ElementAt(0).Should().Be(123); context.Errors["test1.test2"].ElementAt(1).Should().Be(123); context.Errors["test1.test2.test3"].Should().HaveCount(3); context.Errors["test1.test2.test3"].ElementAt(0).Should().Be(123); context.Errors["test1.test2.test3"].ElementAt(1).Should().Be(123); context.Errors["test1.test2.test3"].ElementAt(2).Should().Be(123); } } public class EnterPath_And_AddingErrors { [Theory] [MemberData(nameof(PathTestData.ResolvePath_AllCases), MemberType = typeof(PathTestData))] public void AddErrors_Should_AddToEnteredPath_AfterStepIntoNextPath(string basePath, string newSegment, string expectedPath) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(basePath); context.EnterPath(newSegment); context.AddError(123); context.Errors.Keys.Should().ContainSingle(expectedPath); context.Errors.Should().HaveCount(1); context.Errors[expectedPath].Should().HaveCount(1); context.Errors[expectedPath].ElementAt(0).Should().Be(123); } [Theory] [MemberData(nameof(PathTestData.ResolvePath_AllCases), MemberType = typeof(PathTestData))] public void AddErrors_Should_AddToBasePath_And_EnteredPath(string basePath, string newSegment, string expectedPath) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(basePath); context.AddError(123); context.Errors.Keys.Should().ContainSingle(expectedPath); context.Errors.Should().HaveCount(1); context.Errors[basePath].Should().HaveCount(1); context.Errors[basePath].ElementAt(0).Should().Be(123); context.EnterPath(newSegment); context.AddError(321); if (basePath == expectedPath) { context.Errors.Keys.Should().HaveCount(1); context.Errors.Keys.Should().Contain(basePath); context.Errors[basePath].Should().HaveCount(2); context.Errors[basePath].ElementAt(0).Should().Be(123); context.Errors[basePath].ElementAt(1).Should().Be(321); } else { context.Errors.Keys.Should().HaveCount(2); context.Errors.Keys.Should().Contain(basePath); context.Errors.Keys.Should().Contain(expectedPath); context.Errors[basePath].Should().HaveCount(1); context.Errors[basePath].ElementAt(0).Should().Be(123); context.Errors[expectedPath].Should().HaveCount(1); context.Errors[expectedPath].ElementAt(0).Should().Be(321); } } [Theory] [MemberData(nameof(PathTestData.ResolvePath_AllCases), MemberType = typeof(PathTestData))] public void AddErrors_Should_AddToPreviousPathAfterStepOut(string basePath, string newSegment, string expectedPath) { _ = expectedPath; var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(basePath); context.EnterPath(newSegment); context.LeavePath(); context.AddError(123); context.Errors.Keys.Should().ContainSingle(basePath); context.Errors.Should().HaveCount(1); context.Errors[basePath].Should().HaveCount(1); context.Errors[basePath].ElementAt(0).Should().Be(123); } [Theory] [MemberData(nameof(PathTestData.ResolvePath_AllCases), MemberType = typeof(PathTestData))] public void AddErrors_Should_AddToEnteredPath_And_ToPreviousPathAfterStepOut(string basePath, string newSegment, string expectedPath) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(basePath); context.EnterPath(newSegment); context.AddError(123); context.Errors.Keys.Should().ContainSingle(expectedPath); context.Errors.Should().HaveCount(1); context.Errors[expectedPath].Should().HaveCount(1); context.Errors[expectedPath].ElementAt(0).Should().Be(123); context.LeavePath(); context.AddError(321); if (basePath == expectedPath) { context.Errors.Keys.Should().HaveCount(1); context.Errors.Keys.Should().Contain(basePath); context.Errors[basePath].Should().HaveCount(2); context.Errors[basePath].ElementAt(0).Should().Be(123); context.Errors[basePath].ElementAt(1).Should().Be(321); } else { context.Errors.Keys.Should().HaveCount(2); context.Errors.Keys.Should().Contain(basePath); context.Errors.Keys.Should().Contain(expectedPath); context.Errors[expectedPath].Should().HaveCount(1); context.Errors[expectedPath].ElementAt(0).Should().Be(123); context.Errors[basePath].Should().HaveCount(1); context.Errors[basePath].ElementAt(0).Should().Be(321); } } [Theory] [InlineData("", "#")] [InlineData("base.#.stuff", "base.#.stuff.#")] [InlineData("#.a.#.b.#", "#.a.#.b.#.#")] public void EnterCollectionItemPath_Should_EnterToCollectionIndexPrefix(string basePath, string expectedPath) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(basePath); context.EnterCollectionItemPath(); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors.Keys.Should().Contain(expectedPath); context.Errors[expectedPath].Should().HaveCount(1); context.Errors[expectedPath].ElementAt(0).Should().Be(123); } [Fact] public void EnterCollectionItemPath_Should_LeavePathWithCollectionIndexPrefix() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath("path"); context.EnterCollectionItemPath(); context.LeavePath(); context.AddError(321); context.Errors.Keys.Should().ContainSingle("path"); context.Errors.Should().HaveCount(1); context.Errors["path"].Should().HaveCount(1); context.Errors["path"].ElementAt(0).Should().Be(321); } } public class EnterPath { [Fact] public void Should_FillInitialPath() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath("path"); context.Paths.Keys.Should().HaveCount(1); context.Paths.Keys.Should().Contain(""); context.Paths[""].Should().ContainKey("path"); context.Paths[""]["path"].Should().Be("path"); } [Theory] [MemberData(nameof(PathTestData.ResolvePath_AllCases), MemberType = typeof(PathTestData))] public void Should_FillPaths(string basePath, string newSegment, string expectedPath) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath(basePath); context.EnterPath(newSegment); if (basePath == "") { context.Paths.Keys.Should().ContainSingle(k => k == ""); if (newSegment == "") { context.Paths[""].Keys.Should().ContainSingle(k => k == ""); context.Paths[""][""].Should().Be(""); } else { context.Paths[""].Keys.Should().HaveCount(2); context.Paths[""].Keys.Should().Contain(""); context.Paths[""].Keys.Should().Contain(newSegment); context.Paths[""][newSegment].Should().Be(expectedPath); } } else { context.Paths.Keys.Should().HaveCount(2); context.Paths.Keys.Should().Contain(""); context.Paths.Keys.Should().Contain(basePath); context.Paths[""].Keys.Should().ContainSingle(basePath); context.Paths[""][basePath].Should().Be(basePath); context.Paths[basePath].Keys.Should().ContainSingle(newSegment); context.Paths[basePath][newSegment].Should().Be(expectedPath); } } [Fact] public void Should_FillPaths_And_NotModifyOnLeave() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); context.EnterPath("base"); context.EnterPath("path"); context.EnterPath("nested"); StateCheck(context); context.LeavePath(); StateCheck(context); context.LeavePath(); StateCheck(context); context.LeavePath(); StateCheck(context); void StateCheck(DiscoveryContext ctx) { ctx.Paths.Keys.Should().HaveCount(3); ctx.Paths.Keys.Should().Contain(""); ctx.Paths.Keys.Should().Contain("base"); ctx.Paths.Keys.Should().Contain("base.path"); ctx.Paths[""].Keys.Should().ContainSingle("base"); ctx.Paths[""]["base"].Should().Be("base"); ctx.Paths["base"].Keys.Should().ContainSingle("path"); ctx.Paths["base"]["path"].Should().Be("base.path"); ctx.Paths["base.path"].Keys.Should().ContainSingle("nested"); ctx.Paths["base.path"]["nested"].Should().Be("base.path.nested"); } } } public class EnterScope { public class TestScope { } [Fact] public void Should_GetDiscoverableSpecificationScope_And_ExecuteDiscover_With_ItselfAsParameter() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); var discoverableSpecificationScope = Substitute.For(); actions.GetDiscoverableSpecificationScope(Arg.Is(123)).Returns(discoverableSpecificationScope); context.EnterScope(123); actions.Received(1).GetDiscoverableSpecificationScope(Arg.Is(123)); discoverableSpecificationScope.Received(1).Discover(Arg.Is(context)); } [Theory] [InlineData(2)] [InlineData(5)] [InlineData(10)] public void Should_GetDiscoverableSpecificationScope_And_ExecuteDiscover_With_ItselfAsParameter_ManyTimes(int levels) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); var scopes = Enumerable.Range(1, levels).Select(i => Substitute.For()).ToArray(); for (var i = 1; i < levels; ++i) { actions.GetDiscoverableSpecificationScope(Arg.Is(i)).Returns(scopes[i]); } for (var i = 1; i < levels; ++i) { context.EnterScope(i); } for (var i = 1; i < levels; ++i) { actions.Received(1).GetDiscoverableSpecificationScope(i); scopes[i].Received(1).Discover(Arg.Is(context)); } } [Theory] [InlineData("")] [InlineData("path")] [InlineData("test.path")] public void Should_NotEnterScope_And_Register_ReferenceLoopRoot_When_LoopExists(string name) { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); var discoverableSpecificationScope = Substitute.For(); actions.GetDiscoverableSpecificationScope(Arg.Is(123)).Returns(discoverableSpecificationScope); actions.RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(TestScope))).Returns(666); context.EnterScope(123); actions.Received(1).GetDiscoverableSpecificationScope(Arg.Is(123)); discoverableSpecificationScope.Received(1).Discover(Arg.Is(context)); actions.DidNotReceiveWithAnyArgs().RegisterError(Arg.Any()); context.EnterPath(name); context.EnterScope(123); actions.Received(1).GetDiscoverableSpecificationScope(Arg.Is(123)); discoverableSpecificationScope.Received(1).Discover(Arg.Is(context)); actions.Received(1).RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(TestScope))); context.Errors.Keys.Should().ContainSingle(name); context.Errors[name].Should().HaveCount(1); context.Errors[name].Single().Should().Be(666); context.ReferenceLoopRoots.Should().ContainSingle(name); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(5)] [InlineData(10)] public void Should_NotEnterScope_And_Register_ReferenceLoopRoot_When_LevelsBetween_And_ReferenceLoopExists_NotFromRoot(int levels) { var actions = Substitute.For(); actions.RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(TestScope))).Returns(666); var context = new DiscoveryContext(actions, 100); var scopes = Enumerable.Range(0, levels).Select(i => Substitute.For()).ToArray(); for (var i = 0; i < levels; ++i) { actions.GetDiscoverableSpecificationScope(Arg.Is(i)).Returns(scopes[i]); } for (var i = 0; i < levels; ++i) { context.EnterPath(i.ToString(CultureInfo.InvariantCulture)); context.EnterScope(i); } context.EnterPath(levels.ToString(CultureInfo.InvariantCulture)); context.EnterScope(0); for (var i = 0; i < levels; ++i) { actions.Received(1).GetDiscoverableSpecificationScope(i); scopes[i].Received(1).Discover(Arg.Is(context)); } var errorLevel = string.Join(".", Enumerable.Range(0, levels + 1).Select(i => i).ToArray()); context.Errors.Keys.Should().ContainSingle(errorLevel); context.Errors[errorLevel].Should().HaveCount(1); context.Errors[errorLevel].Single().Should().Be(666); context.ReferenceLoopRoots.Should().ContainSingle(errorLevel); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(5)] [InlineData(10)] public void Should_NotEnterScope_And_Register_ReferenceLoopRoot_When_LevelsBetween_And_ReferenceLoopExists_FromRoot(int levels) { var actions = Substitute.For(); actions.RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(TestScope))).Returns(666); var context = new DiscoveryContext(actions, 0); var scopes = Enumerable.Range(0, levels).Select(i => Substitute.For()).ToArray(); for (var i = 0; i < levels; ++i) { actions.GetDiscoverableSpecificationScope(Arg.Is(i)).Returns(scopes[i]); } for (var i = 1; i < levels; ++i) { context.EnterPath(i.ToString(CultureInfo.InvariantCulture)); context.EnterScope(i); } context.EnterPath(levels.ToString(CultureInfo.InvariantCulture)); context.EnterScope(0); for (var i = 1; i < levels; ++i) { actions.Received(1).GetDiscoverableSpecificationScope(i); scopes[i].Received(1).Discover(Arg.Is(context)); } var errorLevel = string.Join(".", Enumerable.Range(1, levels).Select(i => i).ToArray()); context.Errors.Keys.Should().ContainSingle(errorLevel); context.Errors[errorLevel].Should().HaveCount(1); context.Errors[errorLevel].Single().Should().Be(666); context.ReferenceLoopRoots.Should().ContainSingle(errorLevel); } [Fact] public void Should_NotEnterScope_And_Populate_ReferenceLoopRoots_When_MultipleReferenceLoopsExist() { var actions = Substitute.For(); var context = new DiscoveryContext(actions, 0); var discoverableSpecificationScope = Substitute.For(); actions.GetDiscoverableSpecificationScope(Arg.Is(123)).Returns(discoverableSpecificationScope); actions.RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(TestScope))).Returns(666); actions.RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(int))).Returns(667); actions.RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(DateTimeOffset?))).Returns(668); context.EnterScope(123); context.EnterScope(321); context.EnterPath("base"); context.EnterScope(1); context.EnterScope(2); context.EnterPath("path"); context.EnterScope(123); context.EnterScope(321); context.EnterPath("nested"); context.EnterScope(333); context.EnterScope(333); actions.Received(1).GetDiscoverableSpecificationScope(Arg.Is(123)); actions.Received(1).GetDiscoverableSpecificationScope(Arg.Is(321)); actions.Received(1).GetDiscoverableSpecificationScope(Arg.Is(333)); actions.Received(1).RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(TestScope))); actions.Received(1).RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(int))); actions.Received(1).RegisterError(Arg.Is(a => a is ReferenceLoopError && ((a as ReferenceLoopError).Args.Single() as TypeArg).Value == typeof(DateTimeOffset?))); context.Errors.Keys.Should().Contain("base.path"); context.Errors["base.path"].Should().HaveCount(2); context.Errors["base.path"].ElementAt(0).Should().Be(666); context.Errors["base.path"].ElementAt(1).Should().Be(667); context.Errors["base.path.nested"].Should().HaveCount(1); context.Errors["base.path.nested"].ElementAt(0).Should().Be(668); context.ReferenceLoopRoots.Should().HaveCount(2); context.ReferenceLoopRoots.Should().Contain("base.path"); context.ReferenceLoopRoots.Should().Contain("base.path.nested"); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/ErrorFlagTests.cs ================================================ namespace Validot.Tests.Unit.Validation { using System; using FluentAssertions; using Validot.Validation; using Xunit; public class ErrorFlagTests { [Fact] public void Should_Initialize() { _ = new ErrorFlag(); } [Fact] public void Should_Initialize_WithCapacity() { _ = new ErrorFlag(10); } [Fact] public void Should_ThrowException_When_Initialize_WithNegativeCapacity() { Action action = () => new ErrorFlag(-10); action.Should().ThrowExactly(); } public class IsEnabledAtAnyLevel_After_SetEnabled { [Fact] public void Should_BeFalseByDefault() { var errorFlag = new ErrorFlag(); errorFlag.IsEnabledAtAnyLevel.Should().BeFalse(); } [Theory] [InlineData(1)] [InlineData(10)] [InlineData(666)] public void Should_BeTrueIfEnabled(int level) { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(level, 1); errorFlag.IsEnabledAtAnyLevel.Should().BeTrue(); } [Fact] public void Should_BeTrueIfEnabled_MultipleTimes() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(1, 1); errorFlag.SetEnabled(10, 1); errorFlag.SetEnabled(666, 1); errorFlag.IsEnabledAtAnyLevel.Should().BeTrue(); } [Fact] public void Should_BeFalse_AfterLeavingLevel() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(10, 1); errorFlag.LeaveLevelAndTryGetError(10, out _); errorFlag.IsEnabledAtAnyLevel.Should().BeFalse(); } [Fact] public void Should_BeFalse_AfterLeavingLevel_MultipleTimes() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(1, 1); errorFlag.SetEnabled(10, 1); errorFlag.SetEnabled(666, 1); errorFlag.IsEnabledAtAnyLevel.Should().BeTrue(); errorFlag.LeaveLevelAndTryGetError(1, out _); errorFlag.IsEnabledAtAnyLevel.Should().BeTrue(); errorFlag.LeaveLevelAndTryGetError(10, out _); errorFlag.IsEnabledAtAnyLevel.Should().BeTrue(); errorFlag.LeaveLevelAndTryGetError(666, out _); errorFlag.IsEnabledAtAnyLevel.Should().BeFalse(); } } public class LeaveLevelAndTryGetError { [Theory] [InlineData(10, 666)] [InlineData(666, 10)] [InlineData(0, 0)] public void Should_ReturnTrue_And_SameErrorIdWhenEnabled_When_LevelEnabledAndDetected(int level, int errorId) { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(level, errorId); errorFlag.SetDetected(level); var tryResult = errorFlag.LeaveLevelAndTryGetError(level, out var errorOnLeaving); errorOnLeaving.Should().Be(errorId); tryResult.Should().BeTrue(); } [Fact] public void Should_ReturnTrue_And_FirstErrorIdWhenEnabled_When_LevelEnabledAndDetected_SameLevelMultipleTimes() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(10, 1); errorFlag.SetEnabled(10, 2); errorFlag.SetEnabled(10, 3); errorFlag.SetDetected(10); errorFlag.SetDetected(10); errorFlag.SetDetected(10); var tryResult = errorFlag.LeaveLevelAndTryGetError(10, out var errorOnLeaving); errorOnLeaving.Should().Be(1); tryResult.Should().BeTrue(); } [Fact] public void Should_ReturnFalse_When_LevelNotEnabledAndNotDetected() { var errorFlag = new ErrorFlag(); var tryResult = errorFlag.LeaveLevelAndTryGetError(10, out var errorOnLeaving); errorOnLeaving.Should().Be(-1); tryResult.Should().BeFalse(); } [Fact] public void Should_ReturnFalse_When_LevelEnabledAndNotDetected() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(10, 1); var tryResult = errorFlag.LeaveLevelAndTryGetError(10, out var errorOnLeaving); errorOnLeaving.Should().Be(-1); tryResult.Should().BeFalse(); } [Fact] public void Should_ReturnFalse_When_LevelNotEnabledAndDetected() { var errorFlag = new ErrorFlag(); errorFlag.SetDetected(10); var tryResult = errorFlag.LeaveLevelAndTryGetError(10, out var errorOnLeaving); errorOnLeaving.Should().Be(-1); tryResult.Should().BeFalse(); } } public class IsDetectedAtAnylevel_After_SetDetected { [Fact] public void Should_BeFalseByDefault() { var errorFlag = new ErrorFlag(); errorFlag.IsDetectedAtAnyLevel.Should().BeFalse(); } [Theory] [InlineData(1)] [InlineData(10)] [InlineData(666)] public void Should_BeFalse_WhenEnabled(int level) { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(level, 1); errorFlag.IsDetectedAtAnyLevel.Should().BeFalse(); } [Theory] [InlineData(1)] [InlineData(10)] [InlineData(666)] public void Should_BeTrue_IfDetected_OnSameLevel(int level) { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(level, 1); errorFlag.SetDetected(level); errorFlag.IsDetectedAtAnyLevel.Should().BeTrue(); } [Theory] [InlineData(1)] [InlineData(10)] [InlineData(666)] public void Should_BeTrue_IfDetected_OnHigherLevel(int level) { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(level, 1); errorFlag.SetDetected(level + 1); errorFlag.IsDetectedAtAnyLevel.Should().BeTrue(); } [Theory] [InlineData(1)] [InlineData(10)] [InlineData(666)] public void Should_BeFalse_IfDetected_OnLowerLevel(int level) { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(level, 1); errorFlag.SetDetected(level - 1); errorFlag.IsDetectedAtAnyLevel.Should().BeFalse(); } [Fact] public void Should_BeFalse_AfterLeavingLevel() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(10, 1); errorFlag.SetDetected(10); errorFlag.LeaveLevelAndTryGetError(10, out _); errorFlag.IsDetectedAtAnyLevel.Should().BeFalse(); } [Fact] public void Should_BeFalse_AfterLeavingLevel_MultipleTimes() { var errorFlag = new ErrorFlag(); errorFlag.SetEnabled(1, 1); errorFlag.SetEnabled(10, 1); errorFlag.SetEnabled(666, 1); errorFlag.SetDetected(1000); errorFlag.IsDetectedAtAnyLevel.Should().BeTrue(); errorFlag.LeaveLevelAndTryGetError(1, out _); errorFlag.IsDetectedAtAnyLevel.Should().BeTrue(); errorFlag.LeaveLevelAndTryGetError(10, out _); errorFlag.IsDetectedAtAnyLevel.Should().BeTrue(); errorFlag.LeaveLevelAndTryGetError(666, out _); errorFlag.IsDetectedAtAnyLevel.Should().BeFalse(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/IsValidValidationContextTests.cs ================================================ namespace Validot.Tests.Unit.Validation { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using NSubstitute; using NSubstitute.ExceptionExtensions; using Validot.Validation; using Validot.Validation.Scheme; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Validot.Validation.Stacks; using Xunit; public class IsValidValidationContextTests { public class Initializing { [Fact] public void Should_Initialize() { var modelScheme = Substitute.For(); _ = new IsValidValidationContext(modelScheme, default); } [Fact] public void Should_Initialize_WithDefaultValues() { var modelScheme = Substitute.For(); var validationContext = new IsValidValidationContext(modelScheme, default); validationContext.ReferenceLoopProtectionSettings.Should().BeNull(); validationContext.ErrorFound.Should().BeFalse(); validationContext.ShouldFallBack.Should().BeFalse(); } [Fact] public void Should_Initialize_WithReferenceLoopProtectionSettings() { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(typeof(object)); var referenceLoopProtectionSettings = new ReferenceLoopProtectionSettings(); var validationContext = new IsValidValidationContext(modelScheme, referenceLoopProtectionSettings); validationContext.ReferenceLoopProtectionSettings.Should().BeSameAs(referenceLoopProtectionSettings); } [Theory] [InlineData(true)] [InlineData(false)] public void Should_Initialize_ReferencesStack_With_Null_When_LoopProtectionSettings_Is_Null(bool rootModelTypeIsReference) { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(rootModelTypeIsReference ? typeof(object) : typeof(int)); var validationContext = new IsValidValidationContext( modelScheme, null); validationContext.GetLoopProtectionReferencesStackCount().Should().BeNull(); } [Theory] [InlineData(true)] [InlineData(false)] public void Should_Initialize_ReferencesStack_With_Zero_When_LoopProtectionSettings_Has_NullRootModel(bool rootModelTypeIsReference) { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(rootModelTypeIsReference ? typeof(object) : typeof(int)); var validationContext = new IsValidValidationContext( modelScheme, new ReferenceLoopProtectionSettings()); validationContext.GetLoopProtectionReferencesStackCount().Should().Be(0); } [Fact] public void Should_Initialize_ReferencesStack_With_One_When_LoopProtectionSettings_Has_RootModel_And_RootModelTypeInSchemeIsReferenceType() { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(typeof(object)); var validationContext = new IsValidValidationContext( modelScheme, new ReferenceLoopProtectionSettings(new object())); validationContext.GetLoopProtectionReferencesStackCount().Should().Be(1); } [Fact] public void Should_Initialize_ReferencesStack_With_Zero_When_LoopProtectionSettings_Has_RootModel_And_RootModelTypeInSchemeIsValueType() { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(typeof(int)); var validationContext = new IsValidValidationContext( modelScheme, new ReferenceLoopProtectionSettings(new object())); validationContext.GetLoopProtectionReferencesStackCount().Should().Be(0); } } [Fact] public void ShouldFallback_Should_BeSameAs_ErrorsFound() { var modelScheme = Substitute.For(); var validationContext = new IsValidValidationContext(modelScheme, default); validationContext.ErrorFound.Should().BeFalse(); validationContext.ShouldFallBack.Should().BeFalse(); validationContext.AddError(123); validationContext.ErrorFound.Should().BeTrue(); validationContext.ShouldFallBack.Should().BeTrue(); } public class ErrorFound { [Fact] public void Should_BeFalse_Initially() { var modelScheme = Substitute.For(); var context = new IsValidValidationContext(modelScheme, default); context.ErrorFound.Should().BeFalse(); } [Fact] public void Should_BeFalse_When_AddErrorNotCalled() { var modelScheme = Substitute.For(); var context = new IsValidValidationContext(modelScheme, default); context.EnterPath("asd"); context.EnterPath("123"); context.LeavePath(); context.EnterCollectionItemPath(0); context.EnableErrorDetectionMode(ErrorMode.Append, 1); context.EnableErrorDetectionMode(ErrorMode.Override, 2); context.ErrorFound.Should().BeFalse(); } [Fact] public void Should_BeTrue_When_ErrorAdded() { var modelScheme = Substitute.For(); var context = new IsValidValidationContext(modelScheme, default); context.AddError(123); context.ErrorFound.Should().BeTrue(); } [Fact] public void Should_BeTrue_When_ErrorAdded_AndOtherMethodsDoesntMakeAnyDifference() { var modelScheme = Substitute.For(); var context = new IsValidValidationContext(modelScheme, default); context.AddError(123); context.EnterPath("asd"); context.EnterPath("123"); context.LeavePath(); context.EnterCollectionItemPath(0); context.EnableErrorDetectionMode(ErrorMode.Append, 1); context.EnableErrorDetectionMode(ErrorMode.Override, 2); context.ErrorFound.Should().BeTrue(); } [Fact] public void Should_BeTrue_When_ErrorAdded_MultipleTimes() { var modelScheme = Substitute.For(); var context = new IsValidValidationContext(modelScheme, default); context.AddError(123); context.AddError(123); context.AddError(321); context.AddError(666); context.ErrorFound.Should().BeTrue(); } } public class EnterScope { public class TestClass { } [Fact] public void Should_UseModelScheme() { var specificationScope = Substitute.For>(); var modelScheme = Substitute.For(); modelScheme.GetSpecificationScope(Arg.Is(1234)).Returns(specificationScope); var context = new IsValidValidationContext(modelScheme, default); var model = new TestClass(); context.EnterScope(1234, model); Received.InOrder(() => { modelScheme.GetSpecificationScope(Arg.Is(1234)); specificationScope.Validate(Arg.Is(model), Arg.Is(context)); }); specificationScope.DidNotReceive().Discover(Arg.Any()); specificationScope.Received(1).Validate(Arg.Any(), Arg.Any()); } [Fact] public void Should_UseModelScheme_MultipleTimes() { var specificationScope = Substitute.For>(); var modelScheme = Substitute.For(); modelScheme.GetSpecificationScope(Arg.Is(1234)).Returns(specificationScope); var context = new IsValidValidationContext(modelScheme, default); var model = new TestClass(); context.EnterScope(1234, model); context.EnterScope(1234, model); Received.InOrder(() => { modelScheme.GetSpecificationScope(Arg.Is(1234)); specificationScope.Validate(Arg.Is(model), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(1234)); specificationScope.Validate(Arg.Is(model), Arg.Is(context)); }); specificationScope.DidNotReceive().Discover(Arg.Any()); specificationScope.Received(2).Validate(Arg.Any(), Arg.Any()); } [Fact] public void Should_Return_DifferentScopesForDifferentTypesAndIds() { var model1 = new TestClass(); var specificationScope1 = Substitute.For>(); var model2 = new TestClass(); var specificationScope2 = Substitute.For>(); var model3 = new DateTimeOffset?(DateTimeOffset.FromUnixTimeSeconds(3)); var specificationScope3 = Substitute.For>(); var model4 = 4M; var specificationScope4 = Substitute.For>(); var modelScheme = Substitute.For(); modelScheme.GetSpecificationScope(Arg.Is(1)).Returns(specificationScope1); modelScheme.GetSpecificationScope(Arg.Is(2)).Returns(specificationScope2); modelScheme.GetSpecificationScope(Arg.Is(3)).Returns(specificationScope3); modelScheme.GetSpecificationScope(Arg.Is(4)).Returns(specificationScope4); var context = new IsValidValidationContext(modelScheme, default); context.EnterScope(1, model1); context.EnterScope(2, model2); context.EnterScope(3, model3); context.EnterScope(4, model4); Received.InOrder(() => { modelScheme.GetSpecificationScope(Arg.Is(1)); specificationScope1.Validate(Arg.Is(model1), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(2)); specificationScope2.Validate(Arg.Is(model2), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(3)); specificationScope3.Validate(Arg.Is(model3), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(4)); specificationScope4.Validate(Arg.Is(model4), Arg.Is(context)); }); modelScheme.Received(1).GetSpecificationScope(Arg.Is(1)); modelScheme.Received(1).GetSpecificationScope(Arg.Is(2)); modelScheme.Received(1).GetSpecificationScope(Arg.Is(3)); modelScheme.Received(1).GetSpecificationScope(Arg.Is(4)); } [Fact] public void Should_RethrowException_When_ModelSchemeThrows() { var modelScheme = Substitute.For(); var exception = new KeyNotFoundException(); modelScheme.GetSpecificationScope(Arg.Is(1234)).Throws(exception); var context = new IsValidValidationContext(modelScheme, default); Action action = () => context.EnterScope(1234, new TestClass()); action.Should().ThrowExactly(); } } public class EnterScope_And_Fail_When_ReferenceLoopExists { [Theory] [MemberData(nameof(TraversingTestCases.Loop_Self), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_Simple), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_ThroughMembers), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_ThroughTypes), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_ThroughIndexes), MemberType = typeof(TraversingTestCases))] public void Should_ThrowException_InfiniteReferencesLoopException_WithDetectedLoopInfo_When_ReferencesLoopDetected(string testId, Specification specification, TraversingTestCases.LoopClassA model, string path, string infiniteLoopNestedPath, Type type) { _ = testId; _ = path; _ = infiniteLoopNestedPath; var modelScheme = ModelSchemeFactory.Create(specification); var context = new IsValidValidationContext(modelScheme, new ReferenceLoopProtectionSettings()); Action action = () => context.EnterScope(modelScheme.RootSpecificationScopeId, model); var exception = action.Should().ThrowExactly().And; exception.Path.Should().BeNull(); exception.NestedPath.Should().BeNull(); exception.Type.Should().Be(type); exception.Message.Should().Be($"Reference loop detected: object of type {type.GetFriendlyName()} has been detected twice in the reference graph, effectively creating the infinite references loop (where exactly, that information is not available - is that validation comes from IsValid method, please repeat it using the Validate method and examine the exception thrown)"); } } public class EnterScope_And_TrackingReferencesLoops { [Theory] [MemberData(nameof(TraversingTestCases.TreesExamples_Common), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Struct), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Collections), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Nullable), MemberType = typeof(TraversingTestCases))] public void Should_GetLoopProtectionReferencesStackCount_BeZero_BeforeAndAfterEnteringRootScope_When_NoRootReferenceInSettings(string id, Specification rootSpecification, TraversingTestCases.TestClassA model) { _ = id; var modelScheme = ModelSchemeFactory.Create(rootSpecification); var context = new IsValidValidationContext(modelScheme, new ReferenceLoopProtectionSettings()); context.GetLoopProtectionReferencesStackCount().Should().Be(0); context.EnterScope(modelScheme.RootSpecificationScopeId, model); context.GetLoopProtectionReferencesStackCount().Should().Be(0); } [Theory] [MemberData(nameof(TraversingTestCases.TreesExamples_Common), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Struct), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Collections), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Nullable), MemberType = typeof(TraversingTestCases))] public void Should_GetLoopProtectionReferencesStackCount_BeOne_BeforeAndAfterEnteringRootScope_When_RootModelReference_Exists(string id, Specification rootSpecification, TraversingTestCases.TestClassA model) { _ = id; var modelScheme = ModelSchemeFactory.Create(rootSpecification); var context = new IsValidValidationContext(modelScheme, new ReferenceLoopProtectionSettings(new object())); context.GetLoopProtectionReferencesStackCount().Should().Be(1); context.EnterScope(modelScheme.RootSpecificationScopeId, model); context.GetLoopProtectionReferencesStackCount().Should().Be(1); } [Theory] [MemberData(nameof(TraversingTestCases.TreesExamples_Common), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Struct), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Collections), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Nullable), MemberType = typeof(TraversingTestCases))] public void Should_GetLoopProtectionReferencesStackCount_BeNull_BeforeAndAfterEnteringRootScope_When_RootModelReference_IsNull(string id, Specification rootSpecification, TraversingTestCases.TestClassA model) { _ = id; var modelScheme = ModelSchemeFactory.Create(rootSpecification); var context = new IsValidValidationContext(modelScheme, default); context.GetLoopProtectionReferencesStackCount().Should().BeNull(); context.EnterScope(modelScheme.RootSpecificationScopeId, model); context.GetLoopProtectionReferencesStackCount().Should().BeNull(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scheme/ModelSchemeFactoryTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scheme { using System; using System.Linq; using FluentAssertions; using NSubstitute; using Validot.Errors.Args; using Validot.Validation; using Validot.Validation.Scheme; using Xunit; using Arg = Validot.Arg; public class ModelSchemeFactoryTests { public class TestMember { } public class TestClass { public TestMember Member { get; set; } } [Fact] public void Should_CreateModelScheme() { _ = ModelSchemeFactory.Create(m => m); } [Fact] public void Should_ThrowException_When_NullSpecification() { Action action = () => ModelSchemeFactory.Create(null); action.Should().ThrowExactly(); } [Fact] public void Should_CreateModelScheme_With_Error() { Specification classSpecification = c => c.Rule(x => false).WithMessage("Invalid value custom message"); var modelScheme = ModelSchemeFactory.Create(classSpecification); var error = modelScheme.ErrorRegistry.Where(e => e.Value.Args.Count == 0 && e.Value.Codes.Count == 0 && e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Invalid value custom message"); error.Should().HaveCount(1); modelScheme.Template.Keys.Should().HaveCount(1); modelScheme.Template.Keys.Should().Contain(""); modelScheme.Template[""].Should().Contain(error.Single().Key); } [Fact] public void Should_CreateModelScheme_With_Errors() { Specification classSpecification = c => c .RuleTemplate(x => false, "Invalid value template message {argName}", Arg.Number("argName", 666L)) .Rule(x => false).WithMessage("Invalid value custom message") .Rule(x => false).WithCode("CODE1"); var modelScheme = ModelSchemeFactory.Create(classSpecification); var error1Candidates = modelScheme.ErrorRegistry.Where(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Invalid value custom message"); error1Candidates.Should().HaveCount(1); var error1 = error1Candidates.Single(); error1.Value.Codes.Should().BeEmpty(); error1.Value.Args.Should().BeEmpty(); var error2Candidates = modelScheme.ErrorRegistry.Where(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Invalid value template message {argName}"); error2Candidates.Should().HaveCount(1); var error2 = error2Candidates.Single(); error2.Value.Codes.Should().BeEmpty(); error2.Value.Args.Should().HaveCount(1); var error2Arg = error2.Value.Args.Single(); error2Arg.Should().BeOfType>(); ((NumberArg)error2Arg).Name.Should().Be("argName"); ((NumberArg)error2Arg).Value.Should().Be(666); var error3Candidates = modelScheme.ErrorRegistry.Where(e => e.Value.Codes.Count == 1 && e.Value.Codes.Single() == "CODE1"); error3Candidates.Should().HaveCount(1); var error3 = error3Candidates.Single(); error3.Value.Messages.Should().BeEmpty(); error3.Value.Args.Should().BeEmpty(); modelScheme.Template.Keys.Should().HaveCount(1); modelScheme.Template.Keys.Should().Contain(""); modelScheme.Template[""].Should().Contain(error1.Key); modelScheme.Template[""].Should().Contain(error2.Key); modelScheme.Template[""].Should().Contain(error3.Key); } [Fact] public void Should_CreateModelScheme_With_Errors_And_NestedSpecifications() { Specification memberSpecification = c => c.Optional().RuleTemplate(x => false, "Nested template message", Arg.Number("nestedArg", 100M)).WithExtraCode("CODE_N"); Specification classSpecification = c => c .Optional() .Member(m => m.Member, memberSpecification) .RuleTemplate(x => false, "Invalid value template message {argName}", Arg.Number("argName", 666L)) .Rule(x => false).WithMessage("Invalid value custom message") .Rule(x => false).WithCode("CODE1"); var modelScheme = ModelSchemeFactory.Create(classSpecification); var error1Candidates = modelScheme.ErrorRegistry.Where(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Invalid value template message {argName}"); error1Candidates.Should().HaveCount(1); var error1 = error1Candidates.Single(); error1.Value.Codes.Should().BeEmpty(); error1.Value.Args.Should().HaveCount(1); var error1Arg = error1.Value.Args.Single(); error1Arg.Should().BeOfType>(); ((NumberArg)error1Arg).Name.Should().Be("argName"); ((NumberArg)error1Arg).Value.Should().Be(666); var error2Candidates = modelScheme.ErrorRegistry.Where(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Invalid value custom message"); error2Candidates.Should().HaveCount(1); var error2 = error2Candidates.Single(); error2.Value.Codes.Should().BeEmpty(); error2.Value.Args.Should().BeEmpty(); var error3Candidates = modelScheme.ErrorRegistry.Where(e => e.Value.Codes.Count == 1 && e.Value.Codes.Single() == "CODE1"); error3Candidates.Should().HaveCount(1); var error3 = error3Candidates.Single(); error3.Value.Messages.Should().BeEmpty(); error3.Value.Args.Should().BeEmpty(); var errorNestedCandidates = modelScheme.ErrorRegistry.Where(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Nested template message"); errorNestedCandidates.Should().HaveCount(1); var errorNested = errorNestedCandidates.Single(); errorNested.Value.Codes.Should().HaveCount(1); errorNested.Value.Codes.Single().Should().Be("CODE_N"); errorNested.Value.Args.Should().HaveCount(1); var errorNestedArg = errorNested.Value.Args.Single(); errorNestedArg.Should().BeOfType>(); (errorNestedArg as NumberArg).Name.Should().Be("nestedArg"); (errorNestedArg as NumberArg).Value.Should().Be(100); modelScheme.Template.Keys.Should().HaveCount(2); modelScheme.Template.Keys.Should().Contain(""); modelScheme.Template[""].Should().Contain(error2.Key); modelScheme.Template[""].Should().Contain(error2.Key); modelScheme.Template[""].Should().Contain(error3.Key); modelScheme.Template.Keys.Should().Contain("Member"); modelScheme.Template["Member"].Should().Contain(errorNested.Key); } [Fact] public void Should_CreateModelScheme_With_PathResolved() { Specification memberSpecification = c => c.Rule(x => false).WithMessage("Member error"); Specification classSpecification = c => c .Member(m => m.Member, memberSpecification).WithPath("TestNested") .Rule(x => false).WithPath("TestNested").WithMessage("Base error"); var modelScheme = ModelSchemeFactory.Create(classSpecification); var memberError = modelScheme.ErrorRegistry.Single(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Member error"); var baseError = modelScheme.ErrorRegistry.Single(e => e.Value.Messages.Count == 1 && e.Value.Messages.Single() == "Base error"); modelScheme.Template.Keys.Should().HaveCount(2); modelScheme.Template.Keys.Should().Contain(""); modelScheme.Template[""].Should().NotContain(memberError.Key); modelScheme.Template[""].Should().NotContain(baseError.Key); modelScheme.Template.Keys.Should().Contain("TestNested"); modelScheme.Template["TestNested"].Should().Contain(memberError.Key); modelScheme.Template["TestNested"].Should().Contain(baseError.Key); } public class ReferenceLoopDetected { public class SelfLoop { public SelfLoop Self { get; set; } } public class DirectLoopClassA { public DirectLoopClassB B { get; set; } } public class DirectLoopClassB { public DirectLoopClassA A { get; set; } } public class NestedLoopClassA { public NestedLoopClassB B { get; set; } } public class NestedLoopClassB { public NestedLoopClassC C { get; set; } } public class NestedLoopClassC { public NestedLoopClassA A { get; set; } } [Fact] public void Should_BeFalse_When_NoLoop_InSelfCase() { Specification specificationB = c => c; Specification specificationA = c => c.Member(m => m.Self, m => m.AsModel(specificationB)); var modelScheme = ModelSchemeFactory.Create(specificationA); modelScheme.IsReferenceLoopPossible.Should().BeFalse(); } [Fact] public void Should_BeFalse_When_NoLoop_InDirectCase() { Specification specificationB = c => c; Specification specificationA = c => c.Member(m => m.B, m => m.AsModel(specificationB)); var modelScheme = ModelSchemeFactory.Create(specificationA); modelScheme.IsReferenceLoopPossible.Should().BeFalse(); } [Fact] public void Should_BeFalse_When_NoLoop_InNestedCase() { Specification specificationC = c => c; Specification specificationB = c => c.Member(m => m.C, m => m.AsModel(specificationC)); Specification specificationA = c => c.Member(m => m.B, m => m.AsModel(specificationB)); var modelScheme = ModelSchemeFactory.Create(specificationA); modelScheme.IsReferenceLoopPossible.Should().BeFalse(); } [Fact] public void Should_BeTrue_When_SelfLoop() { Specification specification = null; specification = c => c.Member(m => m.Self, m => m.AsModel(specification)); var modelScheme = ModelSchemeFactory.Create(specification); modelScheme.IsReferenceLoopPossible.Should().BeTrue(); } [Fact] public void Should_BeTrue_When_DirectLoop() { Specification specificationB = null; Specification specificationA = c => c.Member(m => m.B, m => m.AsModel(specificationB)); specificationB = c => c.Member(m => m.A, m => m.AsModel(specificationA)); var modelScheme = ModelSchemeFactory.Create(specificationA); modelScheme.IsReferenceLoopPossible.Should().BeTrue(); } [Fact] public void Should_BeTrue_When_NestedLoop() { Specification specificationC = null; Specification specificationB = c => c.Member(m => m.C, m => m.AsModel(specificationC)); Specification specificationA = c => c.Member(m => m.B, m => m.AsModel(specificationB)); specificationC = c => c.Member(m => m.A, m => m.AsModel(specificationA)); var modelScheme = ModelSchemeFactory.Create(specificationA); modelScheme.IsReferenceLoopPossible.Should().BeTrue(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scheme/ModelSchemeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scheme { using System; using System.Collections.Generic; using FluentAssertions; using NSubstitute; using Validot.Errors; using Validot.Validation.Scheme; using Validot.Validation.Scopes; using Xunit; public class ModelSchemeTests { public class TestClass { } internal static ModelScheme GetDefault() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("", new Dictionary() { ["path"] = "path" }); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); return modelScheme; } public class Initializing { public static IEnumerable Should_Initialize_Data() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("", new Dictionary() { ["path"] = "path" }); yield return new object[] { rootSpecificationScope, specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false }; } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_Initialize(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); modelScheme.RootSpecificationScope.Should().BeSameAs(rootSpecificationScope); modelScheme.ErrorRegistry.Should().BeSameAs(errorRegistry); modelScheme.Template.Should().BeSameAs(template); modelScheme.IsReferenceLoopPossible.Should().Be(isReferenceLoopPossible); modelScheme.RootSpecificationScopeId.Should().Be(rootSpecificationScopeId); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullSpecificationScopes(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; _ = specificationScopes; Action action = () => new ModelScheme(null, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_RootSpecificationScopeId_NotPresentInSpecificationScopes(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; _ = rootSpecificationScopeId; Action action = () => new ModelScheme(specificationScopes, -1, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_RootSpecificationScopeId_OfSpecificationScopeOfInvalidType(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; var invalidRootSpecificationScope = new SpecificationScope(); specificationScopes[rootSpecificationScopeId] = invalidRootSpecificationScope; Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullErrorRegistry(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; _ = errorRegistry; Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, null, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullInErrorRegistry(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; errorRegistry.Add(45, null); Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullTemplate(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; _ = template; Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, null, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullInTemplate(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; template.Add("some_path", null); Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullPathMap(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; _ = pathMap; Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, null, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullInPathMap(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; pathMap.Add("some_path", null); Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(Should_Initialize_Data))] public void Should_ThrowException_When_Initialize_With_NullInPathMapInnerDictionary(object rootSpecificationScope, Dictionary specificationScopes, int rootSpecificationScopeId, Dictionary errorRegistry, Dictionary> template, Dictionary> pathMap, bool isReferenceLoopPossible) { _ = rootSpecificationScope; pathMap.Add("some_path", new Dictionary() { ["some_entry"] = null }); Action action = () => new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, isReferenceLoopPossible); action.Should().ThrowExactly(); } } public class GetSpecificationScope { [Fact] public void Should_GetSpecificationScope() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("", new Dictionary() { ["path"] = "path" }); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); var scope = modelScheme.GetSpecificationScope(10); scope.Should().BeSameAs(rootSpecificationScope); } [Fact] public void Should_GetSpecificationScope_MultipleTimes() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("", new Dictionary() { ["path"] = "path" }); var specificationScope1 = new SpecificationScope(); specificationScopes.Add(30, specificationScope1); var specificationScope2 = new SpecificationScope(); specificationScopes.Add(31, specificationScope2); var specificationScope3 = new SpecificationScope(); specificationScopes.Add(32, specificationScope3); var specificationScope4 = new SpecificationScope(); specificationScopes.Add(33, specificationScope4); var specificationScope5 = new SpecificationScope(); specificationScopes.Add(34, specificationScope5); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); modelScheme.GetSpecificationScope(30).Should().BeSameAs(specificationScope1); modelScheme.GetSpecificationScope(31).Should().BeSameAs(specificationScope2); modelScheme.GetSpecificationScope(32).Should().BeSameAs(specificationScope3); modelScheme.GetSpecificationScope(33).Should().BeSameAs(specificationScope4); modelScheme.GetSpecificationScope(34).Should().BeSameAs(specificationScope5); } [Fact] public void Should_ThrowException_When_InvalidId() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("", new Dictionary() { ["path"] = "path" }); var specificationScope1 = new SpecificationScope(); specificationScopes.Add(30, specificationScope1); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); Action action = () => modelScheme.GetSpecificationScope(123321); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidType() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("", new Dictionary() { ["path"] = "path" }); var specificationScope1 = new SpecificationScope(); specificationScopes.Add(30, specificationScope1); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); Action action = () => modelScheme.GetSpecificationScope(30); action.Should().ThrowExactly(); } } public class GetPathWithIndexes { [Fact] public void Should_ThrowException_When_NullIndexes() { var modelScheme = GetDefault(); Action action = () => modelScheme.GetPathWithIndexes("path", null); action.Should().ThrowExactly(); } [Theory] [InlineData("path")] [InlineData("path.#.segment")] public void Should_ReturnSamePath_When_NoIndexes(string path) { var modelScheme = GetDefault(); var resolvedPath = modelScheme.GetPathWithIndexes(path, Array.Empty()); resolvedPath.Should().Be(path); } [Theory] [MemberData(nameof(PathTestData.GetWithIndexes_AllCases), MemberType = typeof(PathTestData))] public void Should_Resolve_CommonCases(string path, IReadOnlyCollection indexesStack, string expectedPath) { var modelScheme = GetDefault(); var resolvedPath = modelScheme.GetPathWithIndexes(path, indexesStack); resolvedPath.Should().Be(expectedPath); } } public class ResolvePath { [Fact] public void Should_ThrowException_When_NullRelativePath() { var modelScheme = GetDefault(); Action action = () => modelScheme.ResolvePath("some_path", null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullBasePath() { var modelScheme = GetDefault(); Action action = () => modelScheme.ResolvePath(null, "some_level"); action.Should().ThrowExactly(); } [Fact] public void Should_ReturnPathFromMap_IfPresent() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("first", new Dictionary() { ["second"] = "third" }); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); var path = modelScheme.ResolvePath("first", "second"); path.Should().Be("third"); } [Theory] [MemberData(nameof(PathTestData.ResolvePath_AllCases), MemberType = typeof(PathTestData))] public void Should_ReturnPathFromHelper_IfNotPresent(string basePath, string newSegment, string expectedPath) { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); var path = modelScheme.ResolvePath(basePath, newSegment); path.Should().Be(expectedPath); } [Fact] public void Should_ReturnPathFromFromPathsHelper_IfOnlyBasePathNotPresent() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("first_not_present", new Dictionary() { ["second"] = "third" }); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); var path = modelScheme.ResolvePath("first", "second"); path.Should().Be("first.second"); } [Fact] public void Should_ReturnPathFromFromPathsHelper_IfOnlyNextLevelNotPresent() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("first", new Dictionary() { ["next_level_not_present"] = "third" }); var modelScheme = new ModelScheme(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); var path = modelScheme.ResolvePath("first", "second"); path.Should().Be("first.second"); } [Fact] public void Should_ReturnPath_FroMapIfAllPresent_FromHelperIfNot() { var rootSpecificationScope = new SpecificationScope(); var rootSpecificationScopeId = 10; var specificationScopes = new Dictionary(); specificationScopes.Add(rootSpecificationScopeId, rootSpecificationScope); var errorRegistry = new Dictionary(); errorRegistry.Add(44, new Error()); var template = new Dictionary>(); template.Add("path", new[] { 44 }); var pathMap = new Dictionary>(); pathMap.Add("A", new Dictionary() { ["B"] = "C", ["b"] = "c", ["d"] = "d", ["b2"] = "c2", ["(specificationScopes, rootSpecificationScopeId, errorRegistry, template, pathMap, false); modelScheme.ResolvePath("first", "second").Should().Be("first.second"); modelScheme.ResolvePath("A", "B").Should().Be("C"); modelScheme.ResolvePath("a", "B").Should().Be("a.B"); modelScheme.ResolvePath("A", "B1").Should().Be("A.B1"); modelScheme.ResolvePath("A", "b").Should().Be("c"); modelScheme.ResolvePath("A", "d").Should().Be("d"); modelScheme.ResolvePath("A", "(new TestCommand(), (command, context) => { return null; }); } [Fact] public void Should_ThrowException_When_Initialize_With_NullCommand() { Action action = () => new CommandScopeBuilder( null, (command, context) => { return null; }); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Initialize_With_NullCoreBuilder() { Action action = () => new CommandScopeBuilder( null, (command, context) => { return null; }); action.Should().ThrowExactly(); } public class Build { [Fact] public void Should_PassContextAndCommandToCoreBuilder_And_ReturnResultOfCoreBuilder() { var command = new TestCommand(); var context = Substitute.For(); var commandScope = Substitute.For>(); var coreBuilderExecuted = 0; var builder = new CommandScopeBuilder(command, (cmd, ctx) => { cmd.Should().BeSameAs(command); ctx.Should().BeSameAs(context); coreBuilderExecuted++; return commandScope; }); var builtScope = builder.Build(context); coreBuilderExecuted.Should().Be(1); builtScope.Should().BeSameAs(commandScope); var cmdScope = (ICommandScope)builtScope; _ = cmdScope.DidNotReceive().Path; _ = cmdScope.DidNotReceive().ExecutionCondition; } [Fact] public void Should_ThrowException_When_NullContext() { var command = new TestCommand(); var builder = new CommandScopeBuilder(command, (cmd, ctx) => { return Substitute.For>(); }); Action action = () => builder.Build(null); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_ReceivingNullFromCore() { var command = new TestCommand(); var builder = new CommandScopeBuilder(command, (cmd, ctx) => { return null; }); Action action = () => builder.Build(null); action.Should().ThrowExactly(); } } public class TryAdd { [Fact] public void Should_ThrowException_When_NullCommand() { var builder = new CommandScopeBuilder(new TestCommand(), (cmd, ctx) => Substitute.For>()); Action action = () => builder.TryAdd(null); action.Should().ThrowExactly(); } [Fact] public void Should_ReturnTrue_And_SetExecutionCondition_When_WithConditionCommand() { Predicate predicate = o => true; var context = Substitute.For(); var command = new WithConditionCommand(predicate); var builder = new CommandScopeBuilder(new TestCommand(), (cmd, ctx) => Substitute.For>()); var tryAddResult = builder.TryAdd(command); tryAddResult.Should().BeTrue(); var builtScope = (ICommandScope)builder.Build(context); builtScope.ExecutionCondition.Should().BeSameAs(predicate); } [Fact] public void Should_ReturnTrue_And_SetName_When_WithPathCommand() { var context = Substitute.For(); var command = new WithPathCommand("some_path"); var builder = new CommandScopeBuilder(new TestCommand(), (cmd, ctx) => Substitute.For>()); var tryAddResult = builder.TryAdd(command); tryAddResult.Should().BeTrue(); var builtScope = (ICommandScope)builder.Build(context); builtScope.Path.Should().Be("some_path"); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Keep_InvalidCommands_NotAffecting_ValidCommands), MemberType = typeof(ErrorBuilderTestData))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleWithMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleExtraMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleWithCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleExtraCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.SingleMessage_With_SingleCode), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.ManyMessages_With_ManyCodes), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] public void Should_ConstructErrorCodeAndMessages_And_RegisterItInContext_And_SetItsId(object cmds, IError error) { var registrationsCount = 0; var context = Substitute.For(); context.RegisterError(Arg.Any()).Returns(info => { var registeredError = info.Arg(); registeredError.ShouldBeEqualTo(error); registrationsCount++; return 666; }); var builder = new CommandScopeBuilder(new TestCommand(), (cmd, ctx) => Substitute.For>()); var commands = (ICommand[])cmds; foreach (var command in commands) { builder.TryAdd(command); } var builtScope = (ICommandScope)builder.Build(context); builtScope.ErrorId.Should().Be(666); registrationsCount.Should().Be(1); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleExtraMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleExtraCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] public void Should_SetAppendMode(object cmds, IError error) { _ = error; var context = Substitute.For(); context.RegisterError(Arg.Any()).Returns(info => 666); var builder = new CommandScopeBuilder(new TestCommand(), (cmd, ctx) => Substitute.For>()); var commands = (ICommand[])cmds; foreach (var command in commands) { builder.TryAdd(command); } var builtScope = (ICommandScope)builder.Build(context); builtScope.ErrorMode.Should().Be(ErrorMode.Append); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleWithMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleWithCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.SingleMessage_With_SingleCode), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.ManyMessages_With_ManyCodes), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] public void Should_SetOverrideMode(object cmds, IError error) { _ = error; var context = Substitute.For(); context.RegisterError(Arg.Any()).Returns(info => 666); var builder = new CommandScopeBuilder(new TestCommand(), (cmd, ctx) => Substitute.For>()); var commands = (ICommand[])cmds; foreach (var command in commands) { builder.TryAdd(command); } var builtScope = (ICommandScope)builder.Build(context); builtScope.ErrorMode.Should().Be(ErrorMode.Override); } } public class TestCommand : ICommand { } public class TestClass { } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/Builders/ErrorBuilderTestData.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes.Builders { using System; using System.Collections.Generic; using Validot.Errors; using Validot.Errors.Args; using Validot.Specification.Commands; public static class ErrorBuilderTestData { private class TestModel { public string TestMember { get; set; } } public static IEnumerable CommandsEnabled() { var enabled = new ICommand[] { new WithMessageCommand("a"), new WithExtraMessageCommand("a"), new WithCodeCommand("a"), new WithExtraCodeCommand("a") }; var disabled = new ICommand[] { new AsCollectionCommand(a => a), new AsModelCommand(a => a), new AsNullableCommand(a => a), new ForbiddenCommand(), new MemberCommand(a => a.TestMember, a => a), new OptionalCommand(), new RequiredCommand(), new RuleCommand(a => false), }; foreach (var cmd in enabled) { yield return new object[] { cmd, true }; } foreach (var cmd in disabled) { yield return new object[] { cmd, false }; } } public static IEnumerable Keep_InvalidCommands_NotAffecting_ValidCommands() { yield return new object[] { new ICommand[] { new ForbiddenCommand(), new WithCodeCommand("abc"), new OptionalCommand(), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "abc", } } }; yield return new object[] { new ICommand[] { new RuleCommand(a => true), new WithMessageCommand("abc"), new RuleCommand(a => false), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "abc", } } }; yield return new object[] { new ICommand[] { new OptionalCommand(), new WithExtraMessageCommand("hij"), new OptionalCommand(), new WithExtraMessageCommand("klm"), new RuleCommand(a => true), new WithExtraCodeCommand("123"), new WithExtraCodeCommand("456"), new RuleCommand(a => true), new OptionalCommand(), new WithExtraCodeCommand("789"), new RuleCommand(a => false), new WithExtraCodeCommand("101112"), }, new Error { Args = Array.Empty(), Codes = new[] { "123", "456", "789", "101112" }, Messages = new[] { "hij", "klm" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("123"), new RuleCommand(a => false), new WithExtraMessageCommand("hij"), new MemberCommand(m => m.TestMember, m => m), new WithExtraCodeCommand("789"), new WithExtraMessageCommand("klm"), new MemberCommand(m => m.TestMember, m => m), new WithExtraCodeCommand("101112"), new MemberCommand(m => m.TestMember, m => m), new ForbiddenCommand(), new ForbiddenCommand(), }, new Error { Args = Array.Empty(), Codes = new[] { "123", "789", "101112" }, Messages = new[] { "hij", "klm" } } }; } public static class Messages { public static IEnumerable SingleMessage_When_SingleWithMessageCommand() { yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "abc" } } }; } public static IEnumerable SingleMessage_When_SingleExtraMessageCommand() { yield return new object[] { new ICommand[] { new WithExtraMessageCommand("abc"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "abc" } } }; } public static IEnumerable SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne() { yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithMessageCommand("cde"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "cde" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithMessageCommand("cde"), new WithMessageCommand("efg"), new WithMessageCommand("hij"), new WithMessageCommand("klm"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "klm" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithExtraMessageCommand("cde"), new WithMessageCommand("efg"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "efg" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithExtraMessageCommand("cde"), new WithMessageCommand("efg"), new WithExtraMessageCommand("hij"), new WithMessageCommand("klm"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "klm" } } }; } public static IEnumerable ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands() { yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithExtraMessageCommand("cde"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "abc", "cde" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithExtraMessageCommand("cde"), new WithExtraMessageCommand("efg"), new WithExtraMessageCommand("hij"), new WithExtraMessageCommand("klm"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "abc", "cde", "efg", "hij", "klm" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithExtraMessageCommand("cde"), new WithMessageCommand("efg"), new WithExtraMessageCommand("hij"), new WithExtraMessageCommand("klm"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "efg", "hij", "klm" } } }; } } public static class Codes { public static IEnumerable SingleCode_When_SingleWithCodeCommand() { yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "abc" } } }; } public static IEnumerable SingleCode_When_SingleExtraCodeCommand() { yield return new object[] { new ICommand[] { new WithExtraCodeCommand("abc"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "abc" } } }; } public static IEnumerable SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne() { yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithCodeCommand("cde"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "cde" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithCodeCommand("cde"), new WithCodeCommand("efg"), new WithCodeCommand("hij"), new WithCodeCommand("klm"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "klm" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithExtraCodeCommand("cde"), new WithCodeCommand("efg"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "efg" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithExtraCodeCommand("cde"), new WithCodeCommand("efg"), new WithExtraCodeCommand("hij"), new WithCodeCommand("klm"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "klm" } } }; } public static IEnumerable ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands() { yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithExtraCodeCommand("cde"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "abc", "cde" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithExtraCodeCommand("cde"), new WithExtraCodeCommand("efg"), new WithExtraCodeCommand("hij"), new WithExtraCodeCommand("klm"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "abc", "cde", "efg", "hij", "klm" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("abc"), new WithExtraCodeCommand("cde"), new WithCodeCommand("efg"), new WithExtraCodeCommand("hij"), new WithExtraCodeCommand("klm"), }, new Error { Args = Array.Empty(), Messages = Array.Empty(), Codes = new[] { "efg", "hij", "klm" } } }; } } public static class MessagesAndCodes { public static IEnumerable SingleMessage_With_SingleCode() { yield return new object[] { new ICommand[] { new WithMessageCommand("abc"), new WithExtraCodeCommand("123"), }, new Error { Args = Array.Empty(), Codes = new[] { "123" }, Messages = new[] { "abc" } } }; } public static IEnumerable ManyMessages_With_ManyCodes() { yield return new object[] { new ICommand[] { new WithMessageCommand("m1"), new WithExtraMessageCommand("m2"), new WithExtraMessageCommand("m3"), new WithExtraCodeCommand("c1"), new WithExtraCodeCommand("c2"), new WithExtraCodeCommand("c3"), }, new Error { Args = Array.Empty(), Codes = new[] { "c1", "c2", "c3", }, Messages = new[] { "m1", "m2", "m3", } } }; } } public static class Modes { public static IEnumerable Should_BeIn_AppendMode_If_OnlyExtraCommands() { yield return new object[] { new ICommand[] { new WithExtraMessageCommand("m4"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "m4" } } }; yield return new object[] { new ICommand[] { new WithExtraCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c4" }, Messages = Array.Empty() } }; yield return new object[] { new ICommand[] { new WithExtraMessageCommand("m4"), new WithExtraCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c4" }, Messages = new[] { "m4" } } }; yield return new object[] { new ICommand[] { new WithExtraMessageCommand("m1"), new WithExtraMessageCommand("m2"), new WithExtraMessageCommand("m3"), new WithExtraMessageCommand("m4"), new WithExtraCodeCommand("c1"), new WithExtraCodeCommand("c2"), new WithExtraCodeCommand("c3"), new WithExtraCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c1", "c2", "c3", "c4", }, Messages = new[] { "m1", "m2", "m3", "m4", } } }; } public static IEnumerable Should_BeIn_OverrideMode_If_AnyNonExtraCommand_Or_WithErrorClearedCommand() { yield return new object[] { new ICommand[] { new WithMessageCommand("m4"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "m4" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("m3"), new WithExtraMessageCommand("m4"), }, new Error { Args = Array.Empty(), Codes = Array.Empty(), Messages = new[] { "m3", "m4" } } }; yield return new object[] { new ICommand[] { new WithCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c4" }, Messages = Array.Empty() } }; yield return new object[] { new ICommand[] { new WithCodeCommand("c3"), new WithExtraCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c3", "c4" }, Messages = Array.Empty() } }; yield return new object[] { new ICommand[] { new WithMessageCommand("m3"), new WithExtraMessageCommand("m4"), new WithExtraCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c4" }, Messages = new[] { "m3", "m4" } } }; yield return new object[] { new ICommand[] { new WithMessageCommand("m1"), new WithExtraMessageCommand("m2"), new WithExtraMessageCommand("m3"), new WithExtraMessageCommand("m4"), new WithExtraCodeCommand("c1"), new WithExtraCodeCommand("c2"), new WithExtraCodeCommand("c3"), new WithExtraCodeCommand("c4"), }, new Error { Args = Array.Empty(), Codes = new[] { "c1", "c2", "c3", "c4", }, Messages = new[] { "m1", "m2", "m3", "m4", } } }; } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/Builders/ErrorBuilderTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes.Builders { using System; using System.Linq; using FluentAssertions; using Validot.Errors; using Validot.Errors.Args; using Validot.Specification.Commands; using Validot.Validation.Scopes.Builders; using Xunit; public class ErrorBuilderTests { [Fact] public void Should_Constructor_ThrowException_When_NullKey() { Action action = () => new ErrorBuilder(null); action.Should().ThrowExactly(); } [Fact] public void Should_Be_Empty_If_NothingAdded() { var builder = new ErrorBuilder(); builder.IsEmpty.Should().BeTrue(); builder.Mode.Should().Be(ErrorMode.Append); var error = builder.Build(); error.Args.Should().BeEmpty(); error.Messages.Should().BeEmpty(); error.Codes.Should().BeEmpty(); } [Fact] public void Should_Be_NotEmpty_If_SomethingAdded() { var builder = new ErrorBuilder(); builder.TryAdd(new WithExtraCodeCommand("code")); builder.IsEmpty.Should().BeFalse(); var error = builder.Build(); error.Args.Should().BeEmpty(); error.Messages.Should().BeEmpty(); error.Codes.Should().NotBeEmpty(); } [Fact] public void Should_Build_NewObjects() { var builder = new ErrorBuilder(); var error1 = builder.Build(); var error2 = builder.Build(); var error3 = builder.Build(); error1.Should().NotBeSameAs(error2); error1.Should().NotBeSameAs(error3); error2.Should().NotBeSameAs(error3); } [Fact] public void Should_Build_WithMessage_When_AddedOnInit() { var builder = new ErrorBuilder("key"); builder.IsEmpty.Should().BeFalse(); var error = builder.Build(); error.Messages.Should().ContainSingle("key"); error.Codes.Should().BeEmpty(); } [Fact] public void Should_Build_WithArgs_When_AddedOnInit() { var args = new IArg[] { Arg.Text("name1", "value1"), Arg.Number("name2", 2) }; var builder = new ErrorBuilder("key", args); builder.IsEmpty.Should().BeFalse(); var error = builder.Build(); error.Messages.Should().ContainSingle("key"); error.Codes.Should().BeEmpty(); error.Args.Should().BeSameAs(args); error.Args.ElementAt(0).Should().BeOfType(); ((TextArg)error.Args.ElementAt(0)).Name.Should().Be("name1"); ((TextArg)error.Args.ElementAt(0)).Value.Should().Be("value1"); error.Args.ElementAt(1).Should().BeOfType>(); ((NumberArg)error.Args.ElementAt(1)).Name.Should().Be("name2"); ((NumberArg)error.Args.ElementAt(1)).Value.Should().Be(2); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Modes.Should_BeIn_AppendMode_If_OnlyExtraCommands), MemberType = typeof(ErrorBuilderTestData.Modes))] public void Should_Build_WithArgs_When_AddedOnInit_And_CommandsAppended(object cmd, IError expectedError) { var args = new IArg[] { Arg.Text("name1", "value1"), Arg.Number("name2", 2) }; var commands = (ICommand[])cmd; var builder = new ErrorBuilder("key", args); builder.IsEmpty.Should().BeFalse(); foreach (var command in commands) { builder.TryAdd(command); } var error = builder.Build(); var expectedErrorSettable = (Error)expectedError; expectedErrorSettable.Args = args; expectedErrorSettable.Messages = new[] { "key" }.Concat(expectedErrorSettable.Messages.ToArray()).ToArray(); error.ShouldBeEqualTo(expectedErrorSettable); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Modes.Should_BeIn_OverrideMode_If_AnyNonExtraCommand_Or_WithErrorClearedCommand), MemberType = typeof(ErrorBuilderTestData.Modes))] public void Should_Build_WithArgs_When_AddedOnInit_And_CommandsOverride(object cmd, IError expectedError) { var args = new IArg[] { Arg.Text("name1", "value1"), Arg.Number("name2", 2) }; var commands = (ICommand[])cmd; var builder = new ErrorBuilder("key", args); builder.IsEmpty.Should().BeFalse(); foreach (var command in commands) { builder.TryAdd(command); } var error = builder.Build(); var expectedErrorSettable = (Error)expectedError; expectedErrorSettable.Args = args; error.ShouldBeEqualTo(expectedErrorSettable); } [Fact] public void Should_TryAdd_ThrowException_When_NullCommand() { var builder = new ErrorBuilder(); Action action = () => builder.TryAdd(null); action.Should().ThrowExactly(); } [Theory] [MemberData(nameof(ErrorBuilderTestData.CommandsEnabled), MemberType = typeof(ErrorBuilderTestData))] public void Should_TryAdd_ReturnTrue_If_CommandAllowed(object cmd, bool expectedResult) { var command = cmd as ICommand; var builder = new ErrorBuilder(); var result = builder.TryAdd(command); result.Should().Be(expectedResult); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Keep_InvalidCommands_NotAffecting_ValidCommands), MemberType = typeof(ErrorBuilderTestData))] public void Should_TryAdd_Keep_EnabledCommands_And_DisabledCommands_Independent(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } public class TryAddMessages { [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleWithMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] public void Should_TryAdd_Build_SingleMessage_When_SingleWithMessageCommand(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleExtraMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] public void Should_TryAdd_Build_SingleMessage_When_SingleExtraMessageCommand(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Messages))] public void Should_TryAdd_Build_SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands), MemberType = typeof(ErrorBuilderTestData.Messages))] public void Should_TryAdd_Build_ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } } public class TryAddCodes { [Theory] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleWithCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] public void Should_TryAdd_Build_SingleCode_When_SingleWithCodeCommand(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleExtraCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] public void Should_TryAdd_Build_SingleCode_When_SingleExtraCodeCommand(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Codes))] public void Should_TryAdd_Build_SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Codes.ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands), MemberType = typeof(ErrorBuilderTestData.Codes))] public void Should_TryAdd_Build_ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); } } public class Modes { [Theory] [MemberData(nameof(ErrorBuilderTestData.Modes.Should_BeIn_AppendMode_If_OnlyExtraCommands), MemberType = typeof(ErrorBuilderTestData.Modes))] public void Should_BeIn_AppendMode_If_OnlyExtraCommands(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); builder.Mode.Should().Be(ErrorMode.Append); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Modes.Should_BeIn_OverrideMode_If_AnyNonExtraCommand_Or_WithErrorClearedCommand), MemberType = typeof(ErrorBuilderTestData.Modes))] public void Should_BeIn_OverrideMode_If_AnyNonExtraCommand_Or_WithErrorClearedCommand(object cmd, IError error) { var commands = (ICommand[])cmd; var builder = new ErrorBuilder(); foreach (var command in commands) { builder.TryAdd(command); } builder.Build().ShouldBeEqualTo(error); builder.Mode.Should().Be(ErrorMode.Override); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/Builders/ErrorTestsHelpers.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes.Builders { using System.Linq; using FluentAssertions; using Validot.Errors; public static class ErrorTestsHelpers { public static void ShouldBeEqualTo(this IError @this, IError error) { @this.Codes.Should().NotBeNull(); @this.Messages.Should().NotBeNull(); if (error.Messages.Any()) { @this.Messages.Should().NotBeEmpty(); @this.Messages.Count.Should().Be(error.Messages.Count); for (var i = 0; i < error.Messages.Count; ++i) { @this.Messages[i].Should().Be(error.Messages[i]); } } else { @this.Messages.Should().BeEmpty(); } if (error.Codes.Any()) { @this.Codes.Should().NotBeEmpty(); @this.Codes.Count.Should().Be(error.Codes.Count); for (var i = 0; i < error.Codes.Count; ++i) { @this.Codes[i].Should().Be(error.Codes[i]); } } else { @this.Codes.Should().BeEmpty(); } @this.Args.Should().NotBeNull(); if (error.Args.Any()) { @this.Args.Count.Should().Be(error.Args.Count); for (var i = 0; i < error.Args.Count; ++i) { @this.Args[i].Should().BeOfType(error.Args[i].GetType()); @this.Args[i].Name.Should().Be(error.Args[i].Name); var thisStringified = @this.Args[i].ToString(null); var errorStringified = error.Args[i].ToString(null); thisStringified.Should().Be(errorStringified); } } else { @this.Args.Should().BeEmpty(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/Builders/RuleCommandScopeBuilderTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes.Builders { using System; using FluentAssertions; using NSubstitute; using Validot.Errors; using Validot.Specification.Commands; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class RuleCommandScopeBuilderTests { [Fact] public void Should_Initialize() { _ = new CommandScopeBuilder(new TestClass(), (command, context) => { return null; }); } [Fact] public void Should_ThrowException_When_Initialize_With_NullCommand() { Action action = () => new RuleCommandScopeBuilder(null); action.Should().ThrowExactly(); } public class Build { [Fact] public void Should_ReturnRuleCommandScope() { var command = new RuleCommand(x => true); var context = Substitute.For(); var builder = new RuleCommandScopeBuilder(command); var builtScope = builder.Build(context); builtScope.Should().BeOfType>(); } [Fact] public void Should_ReturnRuleCommandScope_WithIsValidPredicate() { Predicate predicate = x => true; var command = new RuleCommand(predicate); var context = Substitute.For(); var builder = new RuleCommandScopeBuilder(command); var builtScope = builder.Build(context); builtScope.Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)builtScope; ruleCommandScope.IsValid.Should().Be(predicate); } [Fact] public void Should_ThrowException_When_NullContext() { Action action = () => _ = new RuleCommandScopeBuilder(null); action.Should().ThrowExactly(); } } public class TryAdd { [Fact] public void Should_ThrowException_When_NullCommand() { var command = new RuleCommand(x => true); var builder = new RuleCommandScopeBuilder(command); Action action = () => builder.TryAdd(null); action.Should().ThrowExactly(); } [Fact] public void Should_ReturnTrue_And_SetCondition_When_WithConditionCommand() { Predicate condition = o => true; var context = Substitute.For(); var command = new WithConditionCommand(condition); var builder = new RuleCommandScopeBuilder(new RuleCommand(x => true)); var tryAddResult = builder.TryAdd(command); tryAddResult.Should().BeTrue(); var builtScope = (ICommandScope)builder.Build(context); builtScope.ExecutionCondition.Should().BeSameAs(condition); } [Fact] public void Should_ReturnTrue_And_SetName_When_WithPathCommand() { var context = Substitute.For(); var command = new WithPathCommand("some_path"); var builder = new RuleCommandScopeBuilder(new RuleCommand(x => true)); var tryAddResult = builder.TryAdd(command); tryAddResult.Should().BeTrue(); var builtScope = (ICommandScope)builder.Build(context); builtScope.Path.Should().Be("some_path"); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Keep_InvalidCommands_NotAffecting_ValidCommands), MemberType = typeof(ErrorBuilderTestData))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleWithMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleExtraMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleWithCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleExtraCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.SingleMessage_With_SingleCode), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.ManyMessages_With_ManyCodes), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] public void Should_ConstructErrorCodeAndMessages_And_RegisterItInContext_And_SetItsId(object cmds, IError error) { var registrationsCount = 0; var context = Substitute.For(); context.RegisterError(Arg.Any()).Returns(info => { var registeredError = info.Arg(); registeredError.ShouldBeEqualTo(error); registrationsCount++; return 666; }); var builder = new RuleCommandScopeBuilder(new RuleCommand(x => true)); var commands = (ICommand[])cmds; foreach (var command in commands) { builder.TryAdd(command); } var builtScope = (ICommandScope)builder.Build(context); builtScope.ErrorId.Should().Be(666); registrationsCount.Should().Be(1); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleExtraMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleExtraCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] public void Should_SetAppendMode(object cmds, IError error) { _ = error; var context = Substitute.For(); context.RegisterError(Arg.Any()).Returns(info => 666); var builder = new RuleCommandScopeBuilder(new RuleCommand(x => true)); var commands = (ICommand[])cmds; foreach (var command in commands) { builder.TryAdd(command); } var builtScope = (ICommandScope)builder.Build(context); builtScope.ErrorMode.Should().Be(ErrorMode.Append); } [Theory] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_When_SingleWithMessageCommand), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.SingleMessage_FromLatestCommand_When_WithMessageCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Messages.ManyMessages_When_WithMessageCommand_IsFollowedBy_WithExtraMessageCommands), MemberType = typeof(ErrorBuilderTestData.Messages))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_When_SingleWithCodeCommand), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.SingleCode_FromLatestCommand_When_WithCodeCommandIsTheLastOne), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.Codes.ManyCodes_When_WithCodeCommand_IsFollowedBy_WithExtraCodeCommands), MemberType = typeof(ErrorBuilderTestData.Codes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.SingleMessage_With_SingleCode), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] [MemberData(nameof(ErrorBuilderTestData.MessagesAndCodes.ManyMessages_With_ManyCodes), MemberType = typeof(ErrorBuilderTestData.MessagesAndCodes))] public void Should_SetOverrideMode(object cmds, IError error) { _ = error; var context = Substitute.For(); context.RegisterError(Arg.Any()).Returns(info => 666); var builder = new RuleCommandScopeBuilder(new RuleCommand(x => true)); var commands = (ICommand[])cmds; foreach (var command in commands) { builder.TryAdd(command); } var builtScope = (ICommandScope)builder.Build(context); builtScope.ErrorMode.Should().Be(ErrorMode.Override); } } private class TestClass : ICommand { } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/Builders/ScopeBuilderContextTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes.Builders { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors; using Validot.Errors.Args; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class ScopeBuilderContextTests { public class TestClass { } [Fact] public void Should_Initialize() { _ = new ScopeBuilderContext(); } [Fact] public void Should_Initialize_WithBasicErrorsRegistered_And_EmptyDictionaries() { var context = new ScopeBuilderContext(); context.DefaultErrorId.Should().Be(0); context.ForbiddenErrorId.Should().Be(1); context.RequiredErrorId.Should().Be(2); context.Errors.Should().NotBeEmpty(); context.Errors.Count.Should().Be(3); context.Errors[context.DefaultErrorId].ShouldBeEqualTo(new Error { Messages = new[] { "Global.Error" }, Codes = Array.Empty(), Args = Array.Empty(), }); context.Errors[context.ForbiddenErrorId].ShouldBeEqualTo(new Error { Messages = new[] { "Global.Forbidden" }, Codes = Array.Empty(), Args = Array.Empty(), }); context.Errors[context.RequiredErrorId].ShouldBeEqualTo(new Error { Messages = new[] { "Global.Required" }, Codes = Array.Empty(), Args = Array.Empty(), }); context.Scopes.Should().BeEmpty(); context.Types.Should().BeEmpty(); } [Fact] public void RegisterError_Should_ThrowException_NullError() { var context = new ScopeBuilderContext(); Action action = () => context.RegisterError(null); action.Should().ThrowExactly(); } [Fact] public void RegisterError_Should_AddError() { var context = new ScopeBuilderContext(); var error = new Error(); var errorId = context.RegisterError(error); context.Errors.Count.Should().Be(4); context.Errors.Keys.Should().Contain(errorId); context.Errors[errorId].Should().BeSameAs(error); context.Scopes.Should().BeEmpty(); context.Types.Should().BeEmpty(); } [Fact] public void RegisterError_Should_AddErrors_And_AssignDifferentIds() { var context = new ScopeBuilderContext(); var error1 = new Error(); var error2 = new Error(); var error3 = new Error(); var errorId1 = context.RegisterError(error1); var errorId2 = context.RegisterError(error2); var errorId3 = context.RegisterError(error3); context.Errors.Count.Should().Be(6); context.Errors.Keys.Should().Contain(errorId1); context.Errors[errorId1].Should().BeSameAs(error1); context.Errors.Keys.Should().Contain(errorId2); context.Errors[errorId2].Should().BeSameAs(error2); context.Errors.Keys.Should().Contain(errorId3); context.Errors[errorId3].Should().BeSameAs(error3); context.Scopes.Should().BeEmpty(); context.Types.Should().BeEmpty(); } public class GetOrRegisterSpecificationScope { [Fact] public void Should_ThrowException_When_NullSpecification() { var context = new ScopeBuilderContext(); Action action = () => context.GetOrRegisterSpecificationScope(null); action.Should().ThrowExactly(); } [Fact] public void Should_CreateSpecificationScope_And_SaveType_And_ReturnItsId() { var context = new ScopeBuilderContext(); Specification specification = m => m; var specificationScopeId = context.GetOrRegisterSpecificationScope(specification); specificationScopeId.Should().Be(0); context.Scopes.Should().NotBeEmpty(); context.Scopes.Count.Should().Be(1); context.Scopes.Keys.Should().Contain(specificationScopeId); context.Scopes[specificationScopeId].Should().BeOfType>(); context.Types.Should().NotBeEmpty(); context.Types.Count.Should().Be(1); context.Types[specificationScopeId].Should().Be(typeof(TestClass)); } [Fact] public void Should_CreateSpecificationScope_And_SaveType_And_ReturnIds_For_DifferentSpecifications() { var context = new ScopeBuilderContext(); Specification specification1 = m => m; Specification specification2 = m => m; Specification specification3 = m => m; var specificationScopeId1 = context.GetOrRegisterSpecificationScope(specification1); var specificationScopeId2 = context.GetOrRegisterSpecificationScope(specification2); var specificationScopeId3 = context.GetOrRegisterSpecificationScope(specification3); specificationScopeId1.Should().Be(0); specificationScopeId2.Should().Be(1); specificationScopeId3.Should().Be(2); context.Scopes.Should().NotBeEmpty(); context.Scopes.Count.Should().Be(3); context.Scopes.Keys.Should().Contain(specificationScopeId1); context.Scopes[specificationScopeId1].Should().BeOfType>(); context.Scopes.Keys.Should().Contain(specificationScopeId2); context.Scopes[specificationScopeId2].Should().BeOfType>(); context.Scopes.Keys.Should().Contain(specificationScopeId3); context.Scopes[specificationScopeId3].Should().BeOfType>(); context.Types.Should().NotBeEmpty(); context.Types.Count.Should().Be(3); context.Types[specificationScopeId1].Should().Be(typeof(object)); context.Types[specificationScopeId2].Should().Be(typeof(DateTime?)); context.Types[specificationScopeId3].Should().Be(typeof(int)); } [Fact] public void Should_CreateSpecificationScope_And_SaveType_OnlyOnce_For_MultipleCallsWithSameSpecification() { var context = new ScopeBuilderContext(); Specification specification = m => m; var specificationScopeId1 = context.GetOrRegisterSpecificationScope(specification); var specificationScopeId2 = context.GetOrRegisterSpecificationScope(specification); var specificationScopeId3 = context.GetOrRegisterSpecificationScope(specification); specificationScopeId1.Should().Be(0); specificationScopeId2.Should().Be(0); specificationScopeId3.Should().Be(0); context.Scopes.Should().NotBeEmpty(); context.Scopes.Count.Should().Be(1); context.Scopes.Keys.Should().Contain(specificationScopeId1); context.Scopes[specificationScopeId1].Should().BeOfType>(); context.Types.Should().NotBeEmpty(); context.Types.Count.Should().Be(1); context.Types[specificationScopeId1].Should().Be(typeof(object)); } } public class GetSpecificationScope { [Fact] public void Should_ThrowException_When_InvalidId() { var context = new ScopeBuilderContext(); Action action = () => { context.GetDiscoverableSpecificationScope(321); }; action.Should().ThrowExactly(); } [Fact] public void Should_GetSpecificationScope() { var context = new ScopeBuilderContext(); Specification specification = m => m; var specificationScopeId = context.GetOrRegisterSpecificationScope(specification); var discoverableSpecificationScope = context.GetDiscoverableSpecificationScope(specificationScopeId); discoverableSpecificationScope.Should().BeSameAs(context.Scopes[specificationScopeId]); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/Builders/ScopeBuilderTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes.Builders { using System; using System.Collections; using System.Collections.Generic; using FluentAssertions; using NSubstitute; using Validot.Errors.Args; using Validot.Specification; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; using Arg = Validot.Arg; public class ScopeBuilderTests { public class TestClass { } public static IEnumerable AllErrorSetupAndContentCases() { foreach (var contentCase in ErrorContentApiHelper.AllCases()) { foreach (var setupCase in ErrorSetupApiHelper.AllCases()) { yield return new object[] { $"{contentCase[0]}_{setupCase[0]}", contentCase[1], contentCase[2], setupCase[1], setupCase[2], }; } } } [Fact] public void Should_Initialize() { _ = new ScopeBuilder(); } [Fact] public void Should_ThrowException_When_NullSpecification() { var builder = new ScopeBuilder(); var context = Substitute.For(); Action action = () => builder.Build(null, context); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_NullContext() { var builder = new ScopeBuilder(); Specification specification = m => m; Action action = () => builder.Build(specification, null); action.Should().ThrowExactly(); } [Fact] public void Should_BeEmpty_When_NoCommand() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m; var scope = builder.Build(specification, context); scope.CommandScopes.Should().BeEmpty(); } public class BuildRule { public static IEnumerable AllSetupCommandCases() { return ErrorSetupApiHelper.AllCases(); } public static IEnumerable AllErrorSetupAndContentCases() { return AllErrorSetupAndContentCases(); } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_RuleCommandScope_When_Rule(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Predicate predicate = x => true; Specification specification = m => appendSetupCommands(m.Rule(predicate)); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)scope.CommandScopes[0]; ruleCommandScope.Path.Should().Be(expectedErrorSetup.Path); ruleCommandScope.ExecutionCondition.Should().Be(expectedErrorSetup.ShouldExecute); ruleCommandScope.ErrorMode.Should().Be(ErrorMode.Append); ruleCommandScope.IsValid.Should().BeSameAs(predicate); ruleCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_RuleCommandScope_When_Rule_WithCustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeEmpty(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Predicate predicate = x => true; Specification specification = m => appendContentCommands(appendSetupCommands(m.Rule(predicate))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)scope.CommandScopes[0]; ruleCommandScope.Path.Should().Be(expectedErrorSetup.Path); ruleCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); ruleCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); ruleCommandScope.IsValid.Should().BeSameAs(predicate); ruleCommandScope.ErrorId.Should().BeGreaterOrEqualTo(0); if (expectedErrorContent.ShouldBeEmpty(0)) { ruleCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { ruleCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); context.Errors.Keys.Should().Contain(ruleCommandScope.ErrorId); expectedErrorContent.Match(context.Errors[ruleCommandScope.ErrorId], initialMessagesAmount: 0); } } } public class BuildRuleTemplate { public static IEnumerable AllSetupCommandCases() { return ErrorSetupApiHelper.AllCases(); } public static IEnumerable AllErrorSetupAndContentCases() { return AllErrorSetupAndContentCases(); } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_RuleCommandScope_When_RuleTemplate_WithArgs(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { _ = testId; var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Predicate predicate = x => true; var args = new IArg[] { Arg.Text("argName1", "argValue"), Arg.Number("argName2", 2), }; Specification specification = m => appendSetupCommands(m.RuleTemplate(predicate, "ruleKey", args)); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)scope.CommandScopes[0]; ruleCommandScope.Path.Should().Be(expectedErrorSetup.Path); ruleCommandScope.ExecutionCondition.Should().Be(expectedErrorSetup.ShouldExecute); ruleCommandScope.ErrorMode.Should().Be(ErrorMode.Append); ruleCommandScope.IsValid.Should().BeSameAs(predicate); ruleCommandScope.ErrorId.Should().BeGreaterOrEqualTo(0); ruleCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); context.Errors.Keys.Should().Contain(ruleCommandScope.ErrorId); var error = context.Errors[ruleCommandScope.ErrorId]; error.Messages.Count.Should().Be(1); error.Messages[0].Should().Be("ruleKey"); error.Args.Should().BeSameAs(args); error.Codes.Should().BeEmpty(); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_RuleCommandScope_When_RuleTemplate_WithArgs_AndCustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Predicate predicate = x => true; var args = new IArg[] { Arg.Text("argName1", "argValue"), Arg.Number("argName2", 2), }; Specification specification = m => appendSetupCommands(appendContentCommands(m.RuleTemplate(predicate, "ruleKey", args))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)scope.CommandScopes[0]; ruleCommandScope.Path.Should().Be(expectedErrorSetup.Path); ruleCommandScope.ExecutionCondition.Should().Be(expectedErrorSetup.ShouldExecute); ruleCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); ruleCommandScope.IsValid.Should().BeSameAs(predicate); ruleCommandScope.ErrorId.Should().BeGreaterOrEqualTo(0); if (expectedErrorContent.ShouldBeEmpty()) { ruleCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { ruleCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); context.Errors.Keys.Should().Contain(ruleCommandScope.ErrorId); var error = context.Errors[ruleCommandScope.ErrorId]; expectedErrorContent.Match(error); error.Args.Should().BeSameAs(args); } } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_RuleCommandScope_When_RuleTemplate_WithoutArgs(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { _ = testId; var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Predicate predicate = x => true; Specification specification = m => appendSetupCommands(m.RuleTemplate(predicate, "ruleKey")); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)scope.CommandScopes[0]; ruleCommandScope.Path.Should().Be(expectedErrorSetup.Path); ruleCommandScope.ExecutionCondition.Should().Be(expectedErrorSetup.ShouldExecute); ruleCommandScope.ErrorMode.Should().Be(ErrorMode.Append); ruleCommandScope.IsValid.Should().BeSameAs(predicate); ruleCommandScope.ErrorId.Should().BeGreaterOrEqualTo(0); ruleCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); context.Errors.Keys.Should().Contain(ruleCommandScope.ErrorId); var error = context.Errors[ruleCommandScope.ErrorId]; error.Messages.Count.Should().Be(1); error.Messages[0].Should().Be("ruleKey"); error.Args.Should().BeEmpty(); error.Codes.Should().BeEmpty(); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_RuleCommandScope_When_RuleTemplate_WithoutArgs_And_CustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Predicate predicate = x => true; Specification specification = m => appendSetupCommands(appendContentCommands(m.RuleTemplate(predicate, "ruleKey"))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var ruleCommandScope = (RuleCommandScope)scope.CommandScopes[0]; ruleCommandScope.Path.Should().Be(expectedErrorSetup.Path); ruleCommandScope.ExecutionCondition.Should().Be(expectedErrorSetup.ShouldExecute); ruleCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); ruleCommandScope.IsValid.Should().BeSameAs(predicate); ruleCommandScope.ErrorId.Should().BeGreaterOrEqualTo(0); if (expectedErrorContent.ShouldBeEmpty()) { ruleCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { ruleCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); context.Errors.Keys.Should().Contain(ruleCommandScope.ErrorId); var error = context.Errors[ruleCommandScope.ErrorId]; expectedErrorContent.Match(error); error.Args.Should().BeEmpty(); } } } public class BuildAsModel { public static IEnumerable AllSetupCommandCases() { return ErrorSetupApiHelper.AllCases(); } public static IEnumerable AllErrorSetupAndContentCases() { return AllErrorSetupAndContentCases(); } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_AsModelScope_When_AsModel(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendSetupCommands(m.AsModel(innerSpecification)); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var modelCommandScope = (ModelCommandScope)scope.CommandScopes[0]; modelCommandScope.Path.Should().Be(expectedErrorSetup.Path); modelCommandScope.ErrorMode.Should().Be(ErrorMode.Append); modelCommandScope.ErrorId.Should().BeNull(); modelCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); context.Scopes.Keys.Should().Contain(modelCommandScope.ScopeId); context.Scopes[modelCommandScope.ScopeId].Should().BeOfType>(); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_AsModelScope_When_AsModel_WithCustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { _ = testId; var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendContentCommands(appendSetupCommands(m.AsModel(innerSpecification))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var modelCommandScope = (ModelCommandScope)scope.CommandScopes[0]; modelCommandScope.Path.Should().Be(expectedErrorSetup.Path); modelCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); modelCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); modelCommandScope.ErrorId.Should().HaveValue(); context.Errors.Keys.Should().Contain(modelCommandScope.ErrorId.Value); if (expectedErrorContent.ShouldBeEmpty(0)) { modelCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { modelCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); expectedErrorContent.Match(context.Errors[modelCommandScope.ErrorId.Value], 0); } context.Scopes.Keys.Should().Contain(modelCommandScope.ScopeId); context.Scopes[modelCommandScope.ScopeId].Should().BeOfType>(); } } public class BuildAsNullable { public static IEnumerable AllSetupCommandCases() { return ErrorSetupApiHelper.AllCases(); } public static IEnumerable AllErrorSetupAndContentCases() { return AllErrorSetupAndContentCases(); } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_AsNullableScope_When_AsNullable(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { _ = testId; var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendSetupCommands(m.AsNullable(innerSpecification)); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var nullableCommandScope = (NullableCommandScope)scope.CommandScopes[0]; nullableCommandScope.Path.Should().Be(expectedErrorSetup.Path); nullableCommandScope.ErrorMode.Should().Be(ErrorMode.Append); nullableCommandScope.ErrorId.Should().BeNull(); nullableCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); context.Scopes.Keys.Should().Contain(nullableCommandScope.ScopeId); context.Scopes[nullableCommandScope.ScopeId].Should().BeOfType>(); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_AsNullableScope_When_AsNullable_WithCustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { _ = testId; var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendContentCommands(appendSetupCommands(m.AsNullable(innerSpecification))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var nullableCommandScope = (NullableCommandScope)scope.CommandScopes[0]; nullableCommandScope.Path.Should().Be(expectedErrorSetup.Path); nullableCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); nullableCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); nullableCommandScope.ErrorId.Should().HaveValue(); context.Errors.Keys.Should().Contain(nullableCommandScope.ErrorId.Value); if (expectedErrorContent.ShouldBeEmpty(0)) { nullableCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { nullableCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); expectedErrorContent.Match(context.Errors[nullableCommandScope.ErrorId.Value], 0); } context.Scopes.Keys.Should().Contain(nullableCommandScope.ScopeId); context.Scopes[nullableCommandScope.ScopeId].Should().BeOfType>(); } } public class BuildAsCollection { public class TestCollection : IEnumerable { private readonly List _list = new List(); public IEnumerator GetEnumerator() { return _list.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public static IEnumerable AllSetupCommandCases() { return ErrorSetupApiHelper.AllCases(); } public static IEnumerable AllErrorSetupAndContentCases() { return AllErrorSetupAndContentCases(); } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_AsCollectionScope_When_AsCollection(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendSetupCommands(m.AsCollection(innerSpecification)); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var collectionCommandScope = (CollectionCommandScope)scope.CommandScopes[0]; collectionCommandScope.Path.Should().Be(expectedErrorSetup.Path); collectionCommandScope.ErrorMode.Should().Be(ErrorMode.Append); collectionCommandScope.ErrorId.Should().BeNull(); collectionCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); context.Scopes.Keys.Should().Contain(collectionCommandScope.ScopeId); context.Scopes[collectionCommandScope.ScopeId].Should().BeOfType>(); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_AsCollectionScope_When_AsCollection_WithCustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendContentCommands(appendSetupCommands(m.AsCollection(innerSpecification))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var collectionCommandScope = (CollectionCommandScope)scope.CommandScopes[0]; collectionCommandScope.Path.Should().Be(expectedErrorSetup.Path); collectionCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); collectionCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); collectionCommandScope.ErrorId.Should().HaveValue(); context.Errors.Keys.Should().Contain(collectionCommandScope.ErrorId.Value); if (expectedErrorContent.ShouldBeEmpty(0)) { collectionCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { collectionCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); expectedErrorContent.Match(context.Errors[collectionCommandScope.ErrorId.Value], 0); } context.Scopes.Keys.Should().Contain(collectionCommandScope.ScopeId); context.Scopes[collectionCommandScope.ScopeId].Should().BeOfType>(); } } public class BuildMember { public class TestParent { public TestClass TestMember { get; set; } } public static IEnumerable AllSetupCommandCases() { return ErrorSetupApiHelper.AllCases(); } public static IEnumerable AllErrorSetupAndContentCases() { return AllErrorSetupAndContentCases(); } [Theory] [MemberData(nameof(AllSetupCommandCases))] public void Should_Build_AsMemberScope_When_AsMemberScope(string testId, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendSetupCommands(m.Member(m1 => m1.TestMember, innerSpecification)); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var memberCommandScope = (MemberCommandScope)scope.CommandScopes[0]; memberCommandScope.Path.Should().Be(expectedErrorSetup.Path ?? "TestMember"); memberCommandScope.ErrorMode.Should().Be(ErrorMode.Append); memberCommandScope.ErrorId.Should().BeNull(); memberCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); context.Scopes.Keys.Should().Contain(memberCommandScope.ScopeId); context.Scopes[memberCommandScope.ScopeId].Should().BeOfType>(); } [Theory] [MemberData(nameof(AllErrorSetupAndContentCases))] public void Should_Build_AsCollectionScope_When_AsCollection_WithCustomError(string testId, Func> appendContentCommands, ErrorContentApiHelper.ExpectedErrorContent expectedErrorContent, Func> appendSetupCommands, ErrorSetupApiHelper.ExpectedErrorSetup expectedErrorSetup) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification innerSpecification = m => m; Specification specification = m => appendContentCommands(appendSetupCommands(m.Member(m1 => m1.TestMember, innerSpecification))); var scope = builder.Build(specification, context); scope.CommandScopes.Should().NotBeEmpty(); scope.CommandScopes.Count.Should().Be(1); scope.CommandScopes[0].Should().BeOfType>(); var memberCommandScope = (MemberCommandScope)scope.CommandScopes[0]; memberCommandScope.Path.Should().Be(expectedErrorSetup.Path ?? "TestMember"); memberCommandScope.ExecutionCondition.Should().BeSameAs(expectedErrorSetup.ShouldExecute); memberCommandScope.ErrorMode.Should().Be(expectedErrorContent.Mode); var testParent = new TestParent() { TestMember = new TestClass() }; memberCommandScope.GetMemberValue(testParent).Should().BeSameAs(testParent.TestMember); memberCommandScope.ErrorId.Should().HaveValue(); context.Errors.Keys.Should().Contain(memberCommandScope.ErrorId.Value); if (expectedErrorContent.ShouldBeEmpty(0)) { memberCommandScope.ErrorId.Should().Be(context.DefaultErrorId); } else { memberCommandScope.ErrorId.Should().NotBe(context.DefaultErrorId); expectedErrorContent.Match(context.Errors[memberCommandScope.ErrorId.Value], 0); } context.Scopes.Keys.Should().Contain(memberCommandScope.ScopeId); context.Scopes[memberCommandScope.ScopeId].Should().BeOfType>(); } } public class PresenceDetection { public static IEnumerable AllCommandCases() { return ErrorContentApiHelper.AllCases(); } [Fact] public void Should_Return_Required_When_NoCommand() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m; var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Required); scope.RequiredErrorId.Should().Be(context.RequiredErrorId); } [Fact] public void Should_Return_Required_When_NoPresenceCommand() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m .Rule(m1 => true); var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Required); scope.RequiredErrorId.Should().Be(context.RequiredErrorId); } [Fact] public void Should_Return_Required_When_Required() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m .Required(); var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Required); scope.RequiredErrorId.Should().Be(context.RequiredErrorId); } [Fact] public void Should_Return_Required_When_Required_And_Rules() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m .Required() .Rule(m1 => false); var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Required); scope.RequiredErrorId.Should().Be(context.RequiredErrorId); } [Theory] [MemberData(nameof(AllCommandCases))] public void Should_Return_Required_WithCustomError(string testId, Func> appendErrorCommands, ErrorContentApiHelper.ExpectedErrorContent expected) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => appendErrorCommands(m.Required()); var scope = builder.Build(specification, context); if (expected.ShouldBeEmpty(0)) { scope.RequiredErrorId.Should().Be(context.RequiredErrorId); } else { scope.RequiredErrorId.Should().NotBe(context.RequiredErrorId); context.Errors.Keys.Should().Contain(scope.RequiredErrorId); expected.Match(context.Errors[scope.RequiredErrorId]); } } [Theory] [MemberData(nameof(AllCommandCases))] public void Should_Return_Required_WithCustomError_And_Rules(string testId, Func> appendErrorCommands, ErrorContentApiHelper.ExpectedErrorContent expected) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => AppendRule(appendErrorCommands(m.Required())); var scope = builder.Build(specification, context); if (expected.ShouldBeEmpty(0)) { scope.RequiredErrorId.Should().Be(context.RequiredErrorId); } else { scope.RequiredErrorId.Should().NotBe(context.RequiredErrorId); context.Errors.Keys.Should().Contain(scope.RequiredErrorId); expected.Match(context.Errors[scope.RequiredErrorId]); } dynamic AppendRule(dynamic api) { if (api is IRuleIn ruleIn) { return RuleExtension.Rule(ruleIn, m => false); } throw new InvalidOperationException("Dynamic api tests failed"); } } [Fact] public void Should_Return_Optional_When_Optional() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m .Optional(); var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Optional); } [Fact] public void Should_Return_Optional_When_Optional_And_Rules() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m .Optional() .Rule(m1 => false); var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Optional); } [Fact] public void Should_Return_Forbidden_When_Forbidden() { var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => m .Forbidden(); var scope = builder.Build(specification, context); scope.Presence.Should().Be(Presence.Forbidden); scope.ForbiddenErrorId.Should().Be(context.ForbiddenErrorId); } [Theory] [MemberData(nameof(AllCommandCases))] public void Should_Return_Forbidden_WithCustomError(string testId, Func> appendErrorCommands, ErrorContentApiHelper.ExpectedErrorContent expected) { testId.Should().NotBeNull(); var context = new ScopeBuilderContext(); var builder = new ScopeBuilder(); Specification specification = m => appendErrorCommands(m.Forbidden()); var scope = builder.Build(specification, context); if (expected.ShouldBeEmpty(0)) { scope.ForbiddenErrorId.Should().Be(context.ForbiddenErrorId); } else { scope.ForbiddenErrorId.Should().NotBe(context.ForbiddenErrorId); context.Errors.Keys.Should().Contain(scope.ForbiddenErrorId); expected.Match(context.Errors[scope.ForbiddenErrorId]); } } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/CollectionCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class CollectionCommandScopeTests { public class TestItem { } public class TestCollection : IEnumerable { private readonly List _items; public TestCollection(IEnumerable items) { _items = items?.ToList() ?? throw new ArgumentNullException(nameof(items)); } public IEnumerator GetEnumerator() { return _items.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } [Fact] public void Should_Initialize() { _ = new CollectionCommandScope, TestItem>(); } [Fact] public void Should_Initialize_WithDefaultValues() { var commandScope = new CollectionCommandScope, TestItem>(); commandScope.ShouldHaveDefaultValues(); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunDiscovery(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new CollectionCommandScope, TestItem>(); commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var discoveryContext = Substitute.For(); commandScope.ShouldDiscover(discoveryContext, context => { context.Received().EnterCollectionItemPath(); context.Received().EnterScope(Arg.Is(123)); context.Received().LeavePath(); }); } public static IEnumerable Should_RunValidation_Data() { var itemsCount = new[] { 0, 1, 5 }; var sets = CommandScopeTestHelper.CommandScopeParameters(); foreach (var count in itemsCount) { foreach (var set in sets) { yield return new[] { set[0], set[1], set[2], set[3], count }; } } } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_OnReferenceTypeItem(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var commandScope = new CollectionCommandScope, TestItem>(); var items = Enumerable.Range(0, itemsCount).Select(i => new TestItem()).ToList(); var model = new TestCollection(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var itemScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { context.Received().EnterCollectionItemPath(Arg.Is(i)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[i])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); itemScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_OnValueTypeItem(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var commandScope = new CollectionCommandScope, decimal>(); var items = Enumerable.Range(0, itemsCount).Select(i => (decimal)i).ToList(); var model = new TestCollection(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var itemScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { context.Received().EnterCollectionItemPath(Arg.Is(i)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[i])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); itemScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } public static IEnumerable Should_RunValidation_And_FallBack_Data() { var itemsCount = new[] { 0, 1, 5 }; var errorIdValues = new int?[] { null, 1 }; var errorModeValues = new[] { ErrorMode.Append, ErrorMode.Override, }; var nameValues = new[] { null, "someName" }; foreach (var count in itemsCount) { foreach (var errorId in errorIdValues) { foreach (var errorMode in errorModeValues) { foreach (var name in nameValues) { yield return new object[] { errorId, errorMode, name, count }; } } } } } [Theory] [MemberData(nameof(Should_RunValidation_And_FallBack_Data))] public void Should_RunValidation_And_FallBack(int? errorId, object errorModeBoxed, string path, int fallBackIndex) { var commandScope = new CollectionCommandScope, int>(); commandScope.ExecutionCondition = m => true; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var shouldFallBack = false; validationContext.ShouldFallBack.Returns(c => shouldFallBack); var validateCount = 0; var items = Enumerable.Range(0, 5).Select(i => i).ToList(); validationContext.When(v => v.EnterScope(Arg.Any(), Arg.Any())).Do(callInfo => { shouldFallBack = ++validateCount > fallBackIndex; }); var model = new TestCollection(items); commandScope.ShouldValidate( model, validationContext, true, context => { var limit = Math.Min(fallBackIndex + 1, items.Count); for (var i = 0; i < limit; ++i) { context.Received().EnterCollectionItemPath(Arg.Is(i)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[i])); context.Received().LeavePath(); } }); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/CommandScopeTestHelper.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using System.Collections.Generic; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; internal static class CommandScopeTestHelper { public static void ShouldHaveDefaultValues(this ICommandScope commandScope) { commandScope.ExecutionCondition.Should().BeNull(); commandScope.ErrorId.Should().NotHaveValue(); commandScope.Path.Should().BeNull(); commandScope.ErrorMode.Should().Be(ErrorMode.Append); } public static IEnumerable CommandScopeParameters() { var shouldExecuteInfo = new bool?[] { true, false, null }; var errorIdValues = new int?[] { null, 1 }; var errorModeValues = new[] { ErrorMode.Append, ErrorMode.Override, }; var nameValues = new[] { null, "someName" }; foreach (var shouldExecute in shouldExecuteInfo) { foreach (var errorId in errorIdValues) { foreach (var errorMode in errorModeValues) { foreach (var name in nameValues) { yield return new object[] { shouldExecute, errorId, errorMode, name }; } } } } } public static void ShouldDiscover(this ICommandScope @this, IDiscoveryContext context, Action callsAssertions) { @this.Discover(context); Received.InOrder(() => { context.EnterPath(Arg.Is(@this.Path)); if (!@this.ErrorId.HasValue || @this.ErrorMode == ErrorMode.Append) { callsAssertions(context); } if (@this.ErrorId.HasValue) { context.AddError(Arg.Is(@this.ErrorId.Value)); } context.LeavePath(); }); } public static void ShouldValidate(this ICommandScope @this, T model, IValidationContext context, bool? shouldExecuteInfo, Action callsAssertions) { @this.Validate(model, context); var shouldExecute = !shouldExecuteInfo.HasValue || shouldExecuteInfo.Value; Received.InOrder(() => { if (shouldExecute) { context.EnterPath(Arg.Is(@this.Path)); if (@this.ErrorId.HasValue) { context.EnableErrorDetectionMode(Arg.Is(@this.ErrorMode), Arg.Is(@this.ErrorId.Value)); } callsAssertions(context); context.LeavePath(); } }); if (!shouldExecute) { context.DidNotReceiveWithAnyArgs().EnterPath(default); context.DidNotReceiveWithAnyArgs().EnableErrorDetectionMode(default, default); context.DidNotReceiveWithAnyArgs().AddError(default, default); context.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); context.DidNotReceiveWithAnyArgs().LeavePath(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/ConvertedCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class ConvertedCommandScopeTests { private class SourceClass { } private class TargetClass { } [Fact] public void Should_Initialize() { _ = new ConvertedCommandScope(); } [Fact] public void Should_Initialize_WithDefaultValues() { var commandScope = new ConvertedCommandScope(); commandScope.ShouldHaveDefaultValues(); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunDiscovery(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new ConvertedCommandScope(); var convertedValue = new TargetClass(); commandScope.Converter = s => convertedValue; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var discoveryContext = Substitute.For(); commandScope.ShouldDiscover(discoveryContext, context => { context.Received().EnterScope(Arg.Is(123)); }); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunValidation_OnConvertedValue(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new ConvertedCommandScope(); var source = new SourceClass(); var target = new TargetClass(); var shouldExecuteCount = 0; var convertCount = 0; commandScope.Converter = sourceToConvert => { sourceToConvert.Should().BeSameAs(source); convertCount++; return target; }; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().BeSameAs(source); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); commandScope.ShouldValidate( source, validationContext, shouldExecuteInfo, context => { context.Received().EnterScope(Arg.Is(123), Arg.Is(target)); }); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); convertCount.Should().Be(shouldExecuteInfo != false ? 1 : 0); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/DictionaryCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class DictionaryCommandScopeTests { public class TestKey { public TestKey(int value) { Value = value; } public int Value { get; } public override bool Equals(object obj) { return obj is TestKey key && Value == key.Value; } public override int GetHashCode() { return Value.GetHashCode(); } } public class TestValue { } public class TestDictionary : IReadOnlyDictionary { private readonly Dictionary _dictionary; public TestDictionary(Dictionary dictionary) { _dictionary = dictionary; } public IEnumerator> GetEnumerator() { return _dictionary.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public int Count => _dictionary.Count; public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); } public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); } public TValue this[TKey key] { get => _dictionary[key]; set => _dictionary[key] = value; } public IEnumerable Keys => _dictionary.Keys; public IEnumerable Values => _dictionary.Values; } [Fact] public void Should_Initialize() { _ = new DictionaryCommandScope, TestKey, TestValue>(); } [Fact] public void Should_Initialize_WithDefaultValues() { var commandScope = new DictionaryCommandScope, TestKey, TestValue>(); commandScope.ShouldHaveDefaultValues(); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunDiscovery(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new DictionaryCommandScope, TestKey, TestValue>(); commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var discoveryContext = Substitute.For(); commandScope.ShouldDiscover(discoveryContext, context => { context.Received().EnterCollectionItemPath(); context.Received().EnterScope(Arg.Is(123)); context.Received().LeavePath(); }); } public static IEnumerable Should_RunValidation_Data() { var itemsCount = new[] { 0, 1, 5 }; var sets = CommandScopeTestHelper.CommandScopeParameters(); foreach (var count in itemsCount) { foreach (var set in sets) { yield return new[] { set[0], set[1], set[2], set[3], count }; } } } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_OnReferenceClassKey(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var commandScope = new DictionaryCommandScope, TestKey, TestValue>() { KeyStringifier = key => $"REF_{key.Value}" }; var items = Enumerable.Range(0, itemsCount).ToDictionary(i => new TestKey(i), _ => new TestValue()); var model = new TestDictionary(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var itemScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { context.Received().EnterPath(Arg.Is("REF_" + i)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[new TestKey(i)])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceiveWithAnyArgs().EnterScope(default, Arg.Any>()); itemScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_OnValueTypeKey(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var commandScope = new DictionaryCommandScope, int, TestValue>() { KeyStringifier = i => $"VAL_{i}" }; var items = Enumerable.Range(0, itemsCount).ToDictionary(i => i, _ => new TestValue()); var model = new Dictionary(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var valueScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { var key = $"VAL_{i}"; context.Received().EnterPath(Arg.Is(key)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[i])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceive().EnterPath(Arg.Is(p => p != path)); valueScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_OnStringKey(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var commandScope = new DictionaryCommandScope, string, TestValue>(); var items = Enumerable.Range(0, itemsCount).ToDictionary(i => $"STR_{i}", _ => new TestValue()); var model = new Dictionary(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var valueScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { var key = $"STR_{i}"; context.Received().EnterPath(Arg.Is(key)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[key])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceive().EnterPath(Arg.Is(p => p != path)); valueScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } public static IEnumerable Should_RunValidation_And_FallBack_Data() { var itemsCount = new[] { 0, 1, 5 }; var errorIdValues = new int?[] { null, 1 }; var errorModeValues = new[] { ErrorMode.Append, ErrorMode.Override, }; var nameValues = new[] { null, "someName" }; foreach (var count in itemsCount) { foreach (var errorId in errorIdValues) { foreach (var errorMode in errorModeValues) { foreach (var name in nameValues) { yield return new object[] { errorId, errorMode, name, count }; } } } } } [Theory] [MemberData(nameof(Should_RunValidation_And_FallBack_Data))] public void Should_RunValidation_And_FallBack(int? errorId, object errorModeBoxed, string path, int fallBackIndex) { var commandScope = new DictionaryCommandScope, TestKey, TestValue>() { KeyStringifier = i => $"KEY_{i.Value}" }; commandScope.ExecutionCondition = m => true; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var shouldFallBack = false; validationContext.ShouldFallBack.Returns(c => shouldFallBack); var validateCount = 0; var items = Enumerable.Range(0, 5).ToDictionary(i => new TestKey(i), _ => new TestValue()); var model = new TestDictionary(items); validationContext.When(v => v.EnterScope(Arg.Any(), Arg.Any())).Do(callInfo => { shouldFallBack = ++validateCount > fallBackIndex; }); commandScope.ShouldValidate( model, validationContext, true, context => { var limit = Math.Min(fallBackIndex + 1, items.Count); for (var i = 0; i < limit; ++i) { context.Received().EnterPath(Arg.Is($"KEY_{i}")); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[new TestKey(i)])); context.Received().LeavePath(); } }); } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_UsingKeyStringifier_When_KeyIsNotString(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var stringifiedKeys = new Dictionary(); var commandScope = new DictionaryCommandScope, int, TestValue>() { KeyStringifier = i => { stringifiedKeys.Add(i, $"VAL_{i}"); return stringifiedKeys[i]; } }; var items = Enumerable.Range(0, itemsCount).ToDictionary(i => i, _ => new TestValue()); var model = new Dictionary(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var valueScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { var key = $"VAL_{i}"; context.Received().EnterPath(Arg.Is(key)); context.Received().EnterScope(Arg.Is(123), Arg.Is(model[i])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceive().EnterPath(Arg.Is(p => p != path)); valueScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); if (shouldExecuteInfo == true) { stringifiedKeys.Count.Should().Be(itemsCount); for (var i = 0; i < itemsCount; ++i) { stringifiedKeys[i].Should().Be($"VAL_{i}"); } } } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_UsingKeyStringifier_When_KeyIsString(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var stringifiedKeys = new Dictionary(); var commandScope = new DictionaryCommandScope, string, TestValue>() { KeyStringifier = i => { stringifiedKeys.Add($"{i}", $"STR_{i}"); return stringifiedKeys[$"{i}"]; } }; var items = Enumerable.Range(0, itemsCount).ToDictionary(i => $"{i}", _ => new TestValue()); var model = new Dictionary(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var valueScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { var key = $"STR_{i}"; context.Received().EnterPath(Arg.Is(key)); context.Received().EnterScope(Arg.Is(123), Arg.Is(model[$"{i}"])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceive().EnterPath(Arg.Is(p => p != path)); valueScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); if (shouldExecuteInfo == true) { stringifiedKeys.Count.Should().Be(itemsCount); for (var i = 0; i < itemsCount; ++i) { stringifiedKeys[$"{i}"].Should().Be($"STR_{i}"); } } } [Theory] [MemberData(nameof(Should_RunValidation_Data))] public void Should_RunValidation_NotUsingKeyStringifier_When_KeyStringifierIsNull(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path, int itemsCount) { var commandScope = new DictionaryCommandScope, string, TestValue>(); var items = Enumerable.Range(0, itemsCount).ToDictionary(i => $"{i}", _ => new TestValue()); var model = new Dictionary(items); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate>)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var valueScope = Substitute.For>(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { for (var i = 0; i < itemsCount; ++i) { var key = $"{i}"; context.Received().EnterPath(Arg.Is(key)); context.Received().EnterScope(Arg.Is(123), Arg.Is(items[key])); context.Received().LeavePath(); } }); if (itemsCount == 0) { validationContext.DidNotReceive().EnterPath(Arg.Is(p => p != path)); valueScope.DidNotReceiveWithAnyArgs().Validate(default, default); } shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/MemberCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class MemberCommandScopeTests { public class TestMember { } public class TestClass { public TestMember Member { get; set; } public decimal ValueMember { get; set; } } [Fact] public void Should_Initialize() { _ = new MemberCommandScope(); } [Fact] public void Should_Initialize_WithDefaultValues() { var commandScope = new MemberCommandScope(); commandScope.ShouldHaveDefaultValues(); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunDiscovery(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new MemberCommandScope(); commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var model = new TestClass(); var getMemberValueCount = 0; commandScope.GetMemberValue = m => { m.Should().BeSameAs(model); m.Member.Should().BeSameAs(model.Member); getMemberValueCount++; return m.Member; }; var discoveryContext = Substitute.For(); commandScope.ShouldDiscover(discoveryContext, context => { context.Received().EnterScope(Arg.Is(123)); }); getMemberValueCount.Should().Be(0); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunValidation_OnReferenceTypeMember(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new MemberCommandScope(); var model = new TestClass() { Member = new TestMember() }; var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var getMemberValueCount = 0; commandScope.GetMemberValue = m => { m.Should().BeSameAs(model); m.Member.Should().BeSameAs(model.Member); getMemberValueCount++; return m.Member; }; commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { context.Received().EnterScope(Arg.Is(123), Arg.Is(model.Member)); }); getMemberValueCount.Should().Be(!shouldExecuteInfo.HasValue || shouldExecuteInfo.Value ? 1 : 0); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunValidation_OnValueTypeMember(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new MemberCommandScope(); var model = new TestClass() { ValueMember = 987M }; var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); var getMemberValueCount = 0; commandScope.GetMemberValue = m => { m.Should().BeSameAs(model); m.ValueMember.Should().Be(987); getMemberValueCount++; return m.ValueMember; }; commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { context.Received().EnterScope(Arg.Is(123), Arg.Is(model.ValueMember)); }); getMemberValueCount.Should().Be(!shouldExecuteInfo.HasValue || shouldExecuteInfo.Value ? 1 : 0); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/ModelCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class ModelCommandScopeTests { public class TestClass { } [Fact] public void Should_Initialize() { _ = new ModelCommandScope(); } [Fact] public void Should_Initialize_WithDefaultValues() { var commandScope = new ModelCommandScope(); commandScope.ShouldHaveDefaultValues(); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunDiscovery(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new ModelCommandScope(); commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var discoveryContext = Substitute.For(); commandScope.ShouldDiscover(discoveryContext, context => { context.Received().EnterScope(Arg.Is(123)); }); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunValidation_OnReferenceType(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new ModelCommandScope(); var model = new TestClass(); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { context.Received().EnterScope(Arg.Is(123), Arg.Is(model)); }); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunValidation_OnValueType(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new ModelCommandScope(); decimal model = 668; var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().Be(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { context.Received().EnterScope(Arg.Is(123), Arg.Is(model)); }); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/NullableCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class NullableCommandScopeTests { [Fact] public void Should_Initialize() { _ = new NullableCommandScope(); } [Fact] public void Should_Initialize_WithDefaultValues() { var commandScope = new NullableCommandScope(); commandScope.ShouldHaveDefaultValues(); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunDiscovery(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new NullableCommandScope(); commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var discoveryContext = Substitute.For(); commandScope.ShouldDiscover(discoveryContext, context => { context.Received().EnterScope(Arg.Is(123)); }); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_RunValidation(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new NullableCommandScope(); var model = (decimal?)667; var executionCounter = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().Be(model); executionCounter++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); commandScope.ShouldValidate( model, validationContext, shouldExecuteInfo, context => { context.Received().EnterScope(Arg.Is(123), Arg.Is(model)); }); executionCounter.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(CommandScopeTestHelper.CommandScopeParameters), MemberType = typeof(CommandScopeTestHelper))] public void Should_NotRunValidation_When_NullableHasNoValue(bool? shouldExecuteInfo, int? errorId, object errorModeBoxed, string path) { var commandScope = new NullableCommandScope(); var executionCounter = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().BeNull(); executionCounter++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; commandScope.ScopeId = 123; var validationContext = Substitute.For(); commandScope.ShouldValidate( null, validationContext, shouldExecuteInfo, context => { }); validationContext.DidNotReceiveWithAnyArgs().EnterScope(default, default); validationContext.DidNotReceiveWithAnyArgs().EnterScope(default, default); executionCounter.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/RuleCommandScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using System.Collections.Generic; using FluentAssertions; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Xunit; public class RuleCommandScopeTests { public class TestClass { } public static IEnumerable Should_Discover_Data() { var sets = CommandScopeTestHelper.CommandScopeParameters(); foreach (var set in sets) { if (set[1] == null) { continue; } yield return new[] { set[0], (int)set[1], set[2], set[3] }; } } [Theory] [MemberData(nameof(Should_Discover_Data))] public void Should_Discover(bool? shouldExecuteInfo, int errorId, object errorModeBoxed, string path) { var commandScope = new RuleCommandScope(); commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; var discoveryContext = Substitute.For(); commandScope.Discover(discoveryContext); Received.InOrder(() => { discoveryContext.EnterPath(path); discoveryContext.AddError(errorId); discoveryContext.LeavePath(); }); discoveryContext.DidNotReceiveWithAnyArgs().EnterScope(default); discoveryContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(); } public static IEnumerable Should_Validate_Data() { var sets = CommandScopeTestHelper.CommandScopeParameters(); foreach (var set in sets) { if (set[1] == null) { continue; } yield return new[] { set[0], (int)set[1], set[2], set[3], true }; yield return new[] { set[0], (int)set[1], set[2], set[3], false }; } } [Theory] [MemberData(nameof(Should_Validate_Data))] public void Should_Validate_ReferenceType(bool? shouldExecuteInfo, int errorId, object errorModeBoxed, string path, bool isValid) { var commandScope = new RuleCommandScope(); var model = new TestClass(); var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().BeSameAs(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; var isValidCount = 0; commandScope.IsValid = m => { m.Should().BeSameAs(model); isValidCount++; return isValid; }; var validationContext = Substitute.For(); commandScope.Validate(model, validationContext); var shouldExecute = !shouldExecuteInfo.HasValue || shouldExecuteInfo.Value; if (shouldExecute) { Received.InOrder(() => { validationContext.EnterPath(path); if (!isValid) { validationContext.AddError(errorId); } validationContext.LeavePath(); isValidCount.Should().Be(1); }); } else { validationContext.DidNotReceiveWithAnyArgs().EnterPath(default); validationContext.DidNotReceiveWithAnyArgs().AddError(default); validationContext.DidNotReceiveWithAnyArgs().LeavePath(); isValidCount.Should().Be(0); } validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); validationContext.DidNotReceiveWithAnyArgs().EnableErrorDetectionMode(default, default); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Theory] [MemberData(nameof(Should_Validate_Data))] public void Should_Validate_ValueType(bool? shouldExecuteInfo, int errorId, object errorModeBoxed, string path, bool isValid) { var commandScope = new RuleCommandScope(); var model = 234M; var shouldExecuteCount = 0; commandScope.ExecutionCondition = !shouldExecuteInfo.HasValue ? (Predicate)null : m => { m.Should().Be(model); shouldExecuteCount++; return shouldExecuteInfo.Value; }; commandScope.ErrorId = errorId; commandScope.ErrorMode = (ErrorMode)errorModeBoxed; commandScope.Path = path; var isValidCount = 0; commandScope.IsValid = m => { m.Should().Be(model); isValidCount++; return isValid; }; var validationContext = Substitute.For(); commandScope.Validate(model, validationContext); var shouldExecute = !shouldExecuteInfo.HasValue || shouldExecuteInfo.Value; if (shouldExecute) { Received.InOrder(() => { validationContext.EnterPath(path); if (!isValid) { validationContext.AddError(errorId); } validationContext.LeavePath(); isValidCount.Should().Be(1); }); } else { validationContext.DidNotReceiveWithAnyArgs().EnterPath(default); validationContext.DidNotReceiveWithAnyArgs().AddError(default); validationContext.DidNotReceiveWithAnyArgs().LeavePath(); isValidCount.Should().Be(0); } validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); validationContext.DidNotReceiveWithAnyArgs().EnableErrorDetectionMode(default, default); shouldExecuteCount.Should().Be(shouldExecuteInfo.HasValue ? 1 : 0); } [Fact] public void Should_ErrorId_ThrowException_When_AssigningNull() { var commandScope = new RuleCommandScope(); Action action = () => { (commandScope as ICommandScope).ErrorId = null; }; action.Should().ThrowExactly(); } [Fact] public void Should_ErrorId_AssignValueAfterCast() { var commandScope = new RuleCommandScope(); commandScope.ErrorId.Should().Be(-1); (commandScope as ICommandScope).ErrorId.Should().Be(-1); (commandScope as ICommandScope).ErrorId = 321; commandScope.ErrorId.Should().Be(321); (commandScope as ICommandScope).ErrorId.Should().Be(321); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Scopes/SpecificationScopeTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Scopes { using System; using System.Collections.Generic; using System.Linq; using NSubstitute; using Validot.Validation; using Validot.Validation.Scopes; using Xunit; public class SpecificationScopeTests { public class TestClass { } public static IEnumerable SpecificationScopeParameters_Data() { var presences = new[] { Presence.Optional, Presence.Required, Presence.Forbidden }; var commandScopesCount = new[] { 0, 1, 5 }; foreach (var presence in presences) { foreach (var count in commandScopesCount) { yield return new object[] { presence, count }; } } } [Theory] [MemberData(nameof(SpecificationScopeParameters_Data))] public void Should_Discover_ReferenceType(object presenceObj, int commandScopesCount) { var presence = (Presence)presenceObj; var commandScopes = Enumerable.Range(0, commandScopesCount).Select(m => { return Substitute.For>(); }).ToList(); var specificationScope = new SpecificationScope(); specificationScope.Presence = presence; specificationScope.CommandScopes = commandScopes; specificationScope.ForbiddenErrorId = 321; specificationScope.RequiredErrorId = 123; var discoveryContext = Substitute.For(); specificationScope.Discover(discoveryContext); Received.InOrder(() => { if (presence == Presence.Forbidden) { discoveryContext.AddError(321, true); } else { if (presence == Presence.Required) { discoveryContext.AddError(123, true); } for (var i = 0; i < commandScopesCount; ++i) { commandScopes[i].Discover(Arg.Is(discoveryContext)); } } }); if (presence == Presence.Optional) { discoveryContext.DidNotReceiveWithAnyArgs().AddError(default); } if (presence == Presence.Forbidden) { for (var i = 0; i < commandScopesCount; ++i) { commandScopes[i].DidNotReceiveWithAnyArgs().Discover(default); } } else if (presence == Presence.Required) { discoveryContext.Received(1).AddError(123, true); } discoveryContext.DidNotReceiveWithAnyArgs().LeavePath(); discoveryContext.DidNotReceiveWithAnyArgs().EnterPath(default); discoveryContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(); discoveryContext.DidNotReceiveWithAnyArgs().EnterScope(default); } [Theory] [MemberData(nameof(SpecificationScopeParameters_Data))] public void Should_Discover_ValueType(object presenceObj, int commandScopesCount) { var presence = (Presence)presenceObj; var commandScopes = Enumerable.Range(0, commandScopesCount).Select(m => { return Substitute.For>(); }).ToList(); var specificationScope = new SpecificationScope(); specificationScope.Presence = presence; specificationScope.CommandScopes = commandScopes; specificationScope.ForbiddenErrorId = 321; specificationScope.RequiredErrorId = 123; var discoveryContext = Substitute.For(); specificationScope.Discover(discoveryContext); Received.InOrder(() => { for (var i = 0; i < commandScopesCount; ++i) { commandScopes[i].Discover(Arg.Is(discoveryContext)); } }); discoveryContext.DidNotReceiveWithAnyArgs().AddError(default); discoveryContext.DidNotReceiveWithAnyArgs().LeavePath(); discoveryContext.DidNotReceiveWithAnyArgs().EnterPath(default); discoveryContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(); discoveryContext.DidNotReceiveWithAnyArgs().EnterScope(default); } public static IEnumerable Should_Validate_ReferenceType_Data() { var parametersData = SpecificationScopeParameters_Data(); foreach (var parameters in parametersData) { yield return new object[] { parameters[0], parameters[1], true }; yield return new object[] { parameters[0], parameters[1], false }; } } [Theory] [MemberData(nameof(Should_Validate_ReferenceType_Data))] public void Should_Validate_ReferenceType(object presenceObj, int commandScopesCount, bool nullModel) { var presence = (Presence)presenceObj; var commandScopes = Enumerable.Range(0, commandScopesCount).Select(m => { return Substitute.For>(); }).ToList(); var specificationScope = new SpecificationScope(); specificationScope.Presence = presence; specificationScope.CommandScopes = commandScopes; specificationScope.ForbiddenErrorId = 321; specificationScope.RequiredErrorId = 123; var validationContext = Substitute.For(); TestClass model = nullModel ? null : new TestClass(); specificationScope.Validate(model, validationContext); Received.InOrder(() => { if (nullModel) { if (presence == Presence.Required) { validationContext.AddError(Arg.Is(123), true); } } else { if (presence == Presence.Forbidden) { validationContext.AddError(Arg.Is(321), true); } else { for (var i = 0; i < commandScopesCount; ++i) { commandScopes[i].Validate(Arg.Is(model), Arg.Is(validationContext)); } } } }); if (presence == Presence.Optional) { validationContext.DidNotReceiveWithAnyArgs().AddError(default, default); } if (nullModel) { for (var i = 0; i < commandScopesCount; ++i) { commandScopes[i].DidNotReceiveWithAnyArgs().Validate(default, default); } } validationContext.DidNotReceiveWithAnyArgs().LeavePath(); validationContext.DidNotReceiveWithAnyArgs().EnterPath(default); validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); validationContext.DidNotReceiveWithAnyArgs().EnableErrorDetectionMode(default, default); } [Theory] [MemberData(nameof(SpecificationScopeParameters_Data))] public void Should_Validate_ValueType(object presenceObj, int commandScopesCount) { var presence = (Presence)presenceObj; var commandScopes = Enumerable.Range(0, commandScopesCount).Select(m => { return Substitute.For>(); }).ToList(); var specificationScope = new SpecificationScope(); specificationScope.Presence = presence; specificationScope.CommandScopes = commandScopes; specificationScope.ForbiddenErrorId = 321; specificationScope.RequiredErrorId = 123; var validationContext = Substitute.For(); var model = 234M; specificationScope.Validate(model, validationContext); Received.InOrder(() => { for (var i = 0; i < commandScopesCount; ++i) { commandScopes[i].Validate(Arg.Is(model), Arg.Is(validationContext)); } }); validationContext.DidNotReceiveWithAnyArgs().AddError(default); validationContext.DidNotReceiveWithAnyArgs().LeavePath(); validationContext.DidNotReceiveWithAnyArgs().EnterPath(default); validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); validationContext.DidNotReceiveWithAnyArgs().EnableErrorDetectionMode(default, default); } [Theory] [MemberData(nameof(SpecificationScopeParameters_Data))] public void Should_Validate_And_FallBack(object presenceObj, int fallBackIndex) { var presence = (Presence)presenceObj; var validateCount = 0; var shouldFallBack = false; var validationContext = Substitute.For(); validationContext.ShouldFallBack.Returns(c => shouldFallBack); var commandScopes = Enumerable.Range(0, 5).Select(m => { var cmdScope = Substitute.For>(); cmdScope.When(x => x.Validate(Arg.Any(), Arg.Any())).Do(callInfo => { shouldFallBack = ++validateCount > fallBackIndex; }); return cmdScope; }).ToList(); var specificationScope = new SpecificationScope(); specificationScope.Presence = presence; specificationScope.CommandScopes = commandScopes; specificationScope.ForbiddenErrorId = 321; specificationScope.RequiredErrorId = 123; var model = 234M; specificationScope.Validate(model, validationContext); Received.InOrder(() => { var limit = Math.Min(fallBackIndex + 1, commandScopes.Count); for (var i = 0; i < limit; ++i) { commandScopes[i].Validate(Arg.Is(model), Arg.Is(validationContext)); } }); validationContext.DidNotReceiveWithAnyArgs().AddError(default); validationContext.DidNotReceiveWithAnyArgs().LeavePath(); validationContext.DidNotReceiveWithAnyArgs().EnterPath(default); validationContext.DidNotReceiveWithAnyArgs().EnterCollectionItemPath(default); validationContext.DidNotReceiveWithAnyArgs().EnableErrorDetectionMode(default, default); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Stack/PathStackTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Stack { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Validot.Validation.Stacks; using Xunit; public class PathStackTests { [Fact] public void Should_Initialize() { _ = new PathStack(); } [Fact] public void Should_Initialize_WithDefaultValues() { var pathStack = new PathStack(); pathStack.Path.Should().BeEmpty(); pathStack.Level.Should().Be(0); pathStack.HasIndexes.Should().BeFalse(); pathStack.IndexesStack.Should().BeEmpty(); } public class Push { [Fact] public void Should_SetPath() { var pathStack = new PathStack(); pathStack.Push("name"); pathStack.Path.Should().Be("name"); pathStack.HasIndexes.Should().BeFalse(); pathStack.IndexesStack.Should().BeEmpty(); pathStack.Level.Should().Be(1); } [Theory] [InlineData(50)] [InlineData(200)] [InlineData(500)] public void Should_SetPath_MultipleTimes(int max) { var pathStack = new PathStack(); for (var i = 0; i < max; ++i) { pathStack.Push($"path_{i}"); pathStack.Path.Should().Be($"path_{i}"); pathStack.Level.Should().Be(i + 1); pathStack.HasIndexes.Should().BeFalse(); pathStack.IndexesStack.Should().BeEmpty(); } } } public class PushWithDiscoveryIndex { [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public void Push_Should_IncrementLevel(int count) { var pathStack = new PathStack(); for (var i = 0; i < count; ++i) { pathStack.Push($"name_{i}"); } pathStack.Level.Should().Be(count); } [Fact] public void Should_SetPath_WithCollectionIndexPrefixAsIndex() { var pathStack = new PathStack(); pathStack.PushWithDiscoveryIndex("name"); pathStack.Path.Should().Be("name"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainSingle("#"); pathStack.Level.Should().Be(1); } [Theory] [InlineData(50)] [InlineData(200)] [InlineData(500)] public void Should_SetPath_WithCollectionIndexPrefixAsIndex_MultipleTimes(int max) { var pathStack = new PathStack(); for (var i = 0; i < max; ++i) { pathStack.PushWithDiscoveryIndex($"path_{i}"); pathStack.Path.Should().Be($"path_{i}"); pathStack.Level.Should().Be(i + 1); pathStack.HasIndexes.Should().BeTrue(); var indexes = Enumerable.Range(0, i).Select(s => "#").ToArray(); pathStack.IndexesStack.Should().ContainInOrder(indexes); } } } public class PushWithIndex { [Theory] [InlineData(0)] [InlineData(2)] [InlineData(100)] [InlineData(500)] public void Should_SetPath_WithIndex(int index) { var pathStack = new PathStack(); pathStack.PushWithIndex("name", index); pathStack.Path.Should().Be("name"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainSingle($"{index}"); pathStack.Level.Should().Be(1); } [Theory] [InlineData(50)] [InlineData(200)] [InlineData(500)] public void Should_SetPath_WithCollectionIndexPrefixAsIndex_MultipleTimes(int max) { var pathStack = new PathStack(); for (var i = 0; i < max; ++i) { pathStack.PushWithIndex($"path_{i}", i); pathStack.Path.Should().Be($"path_{i}"); pathStack.Level.Should().Be(i + 1); pathStack.HasIndexes.Should().BeTrue(); var tempStack = new Stack(Enumerable.Range(0, i + 1).Select(s => $"{s}").ToArray()); pathStack.IndexesStack.Should().ContainInOrder(tempStack); } } } [Fact] public void Push_Mixed() { var pathStack = new PathStack(); pathStack.Push("first"); pathStack.Path.Should().Be("first"); pathStack.HasIndexes.Should().BeFalse(); pathStack.IndexesStack.Should().BeEmpty(); pathStack.Level.Should().Be(1); pathStack.Push("second"); pathStack.Path.Should().Be("second"); pathStack.HasIndexes.Should().BeFalse(); pathStack.IndexesStack.Should().BeEmpty(); pathStack.Level.Should().Be(2); pathStack.PushWithIndex("third", 3); pathStack.Path.Should().Be("third"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainInOrder("3"); pathStack.Level.Should().Be(3); pathStack.PushWithDiscoveryIndex("fourth"); pathStack.Path.Should().Be("fourth"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainInOrder("#", "3"); pathStack.Level.Should().Be(4); pathStack.PushWithIndex("fifth", 5); pathStack.Path.Should().Be("fifth"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainInOrder("5", "#", "3"); pathStack.Level.Should().Be(5); pathStack.Push("sixth"); pathStack.Path.Should().Be("sixth"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainInOrder("5", "#", "3"); pathStack.Level.Should().Be(6); pathStack.PushWithDiscoveryIndex("seventh"); pathStack.Path.Should().Be("seventh"); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainInOrder("#", "5", "#", "3"); pathStack.Level.Should().Be(7); } public class Pop { [Fact] public void Should_GoToPreviousPath() { var pathStack = new PathStack(); pathStack.Push("first"); pathStack.Push("second"); pathStack.Path.Should().Be("second"); pathStack.Pop(); pathStack.Path.Should().Be("first"); } [Theory] [InlineData(5, 3, 2)] [InlineData(10, 8, 2)] public void Should_GoBackToPreviousPath_MultipleTimes(int max, int stepsUp, int finalLevel) { var pathStack = new PathStack(); for (var i = 0; i < max; ++i) { pathStack.Push($"level_{i + 1}"); } for (var i = 0; i < stepsUp; ++i) { pathStack.Pop(); } pathStack.Path.Should().Be($"level_{finalLevel}"); } [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public void Should_GoBackToEmptyString_If_ReachedRootLevel(int levels) { var pathStack = new PathStack(); for (var i = 0; i < levels; ++i) { pathStack.Push($"level_{i}"); } for (var i = 0; i < levels; ++i) { pathStack.Pop(); } pathStack.Path.Should().Be(""); } [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] public void Should_ThrowException_When_ExceededRootLevel(int levels) { var pathStack = new PathStack(); for (var i = 0; i < levels; ++i) { pathStack.Push($"level_{i}"); } for (var i = 0; i < levels; ++i) { pathStack.Pop(); } pathStack.Path.Should().Be(""); Action action = () => { pathStack.Pop(); }; action.Should().ThrowExactly(); } [Theory] [InlineData(1)] [InlineData(10)] [InlineData(200)] [InlineData(500)] public void Should_PopIndexes(int levels) { var pathStack = new PathStack(); for (var i = 0; i < levels; ++i) { if (i % 2 == 0) { pathStack.PushWithIndex($"level_{i}", i); } else { pathStack.PushWithDiscoveryIndex($"level_{i}"); } } var items = Enumerable.Range(0, levels).Select(i => i % 2 == 0 ? $"{i}" : "#").ToArray(); for (var i = levels - 1; i >= 0; --i) { pathStack.Path.Should().Be($"level_{i}"); pathStack.Level.Should().Be(i + 1); pathStack.HasIndexes.Should().BeTrue(); pathStack.IndexesStack.Should().ContainInOrder(items.Take(i + 1).Reverse()); pathStack.Pop(); } pathStack.Path.Should().Be(""); pathStack.Level.Should().Be(0); pathStack.HasIndexes.Should().BeFalse(); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Stack/ReferenceLoopExceptionTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Stack { using System; using FluentAssertions; using Validot.Validation.Stacks; using Xunit; public class ReferenceLoopExceptionTests { [Fact] public void Should_Initialize_WithPaths() { var exception = new ReferenceLoopException("zxc", "zxc.nested", 123, typeof(DateTimeOffset?)); exception.Path.Should().Be("zxc"); exception.NestedPath.Should().Be("zxc.nested"); exception.ScopeId.Should().Be(123); exception.Type.Should().Be(typeof(DateTimeOffset?)); exception.Message.Should().Be($"Reference loop detected: object of type Nullable has been detected twice in the reference graph, effectively creating an infinite references loop (at first under the path 'zxc' and then under the nested path 'zxc.nested')"); } [Fact] public void Should_Initialize_WithPaths_And_WithRootPath() { var exception = new ReferenceLoopException(null, "zxc.nested", 123, typeof(DateTimeOffset?)); exception.Path.Should().BeNull(); exception.NestedPath.Should().Be("zxc.nested"); exception.ScopeId.Should().Be(123); exception.Type.Should().Be(typeof(DateTimeOffset?)); exception.Message.Should().Be($"Reference loop detected: object of type Nullable has been detected twice in the reference graph, effectively creating an infinite references loop (at first under the root path, so the validated object itself, and then under the nested path 'zxc.nested')"); } [Fact] public void Should_Initialize_WithoutPaths() { var exception = new ReferenceLoopException(123, typeof(DateTimeOffset?)); exception.Path.Should().BeNull(); exception.NestedPath.Should().BeNull(); exception.ScopeId.Should().Be(123); exception.Type.Should().Be(typeof(DateTimeOffset?)); exception.Message.Should().Be($"Reference loop detected: object of type {typeof(DateTimeOffset?).GetFriendlyName()} has been detected twice in the reference graph, effectively creating the infinite references loop (where exactly, that information is not available - is that validation comes from IsValid method, please repeat it using the Validate method and examine the exception thrown)"); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Stack/ReferenceLoopProtectionSettingsTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Stack { using FluentAssertions; using Validot.Validation.Stacks; using Xunit; public class ReferenceLoopProtectionSettingsTests { [Fact] public void Should_Initialize_RootReferenceModel_WithNull() { var settings = new ReferenceLoopProtectionSettings(); settings.RootModelReference.Should().BeNull(); } [Fact] public void Should_Initialize_RootReferenceModel() { var model = new object(); var settings = new ReferenceLoopProtectionSettings(model); settings.RootModelReference.Should().BeSameAs(model); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/Stack/ReferencesStackTests.cs ================================================ namespace Validot.Tests.Unit.Validation.Stack { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Validation.Stacks; using Xunit; public class ReferencesStackTests { [Fact] public void Should_Initialize() { _ = new ReferencesStack(); } public class TryPush { [Fact] public void Should_Push_And_ReturnTrue() { var referencesStack = new ReferencesStack(); var result = referencesStack.TryPush(10, "some.path", new object(), out var higherLevelPath); result.Should().BeTrue(); higherLevelPath.Should().BeNull(); } [Fact] public void Should_Push_And_ReturnTrue_When_DifferentScopeId_And_DifferentObject() { var referencesStack = new ReferencesStack(); var result1 = referencesStack.TryPush(10, "some.path", new object(), out var higherLevelPath1); var result2 = referencesStack.TryPush(20, "some.path.next", new object(), out var higherLevelPath2); result1.Should().BeTrue(); higherLevelPath1.Should().BeNull(); result2.Should().BeTrue(); higherLevelPath2.Should().BeNull(); } [Fact] public void Should_Push_And_ReturnTrue_When_DifferentScopeId_And_SameObject() { var referencesStack = new ReferencesStack(); var model = new object(); var result1 = referencesStack.TryPush(10, "some.path", model, out var higherLevelPath1); var result2 = referencesStack.TryPush(20, "some.path.next", model, out var higherLevelPath2); result1.Should().BeTrue(); higherLevelPath1.Should().BeNull(); result2.Should().BeTrue(); higherLevelPath2.Should().BeNull(); } [Fact] public void Should_Push_And_ReturnTrue_When_SameScopeId_And_DifferentObject() { var referencesStack = new ReferencesStack(); var result1 = referencesStack.TryPush(10, "some.path", new object(), out var higherLevelPath1); var result2 = referencesStack.TryPush(10, "some.path.next", new object(), out var higherLevelPath2); result1.Should().BeTrue(); higherLevelPath1.Should().BeNull(); result2.Should().BeTrue(); higherLevelPath2.Should().BeNull(); } [Fact] public void Should_Push_And_ReturnFalse_And_PreviousPath_When_SameScopeId_And_SameObject() { var referencesStack = new ReferencesStack(); var model = new object(); var result1 = referencesStack.TryPush(10, "some.path", model, out var higherLevelPath1); var result2 = referencesStack.TryPush(10, "some.path.next", model, out var higherLevelPath2); result1.Should().BeTrue(); higherLevelPath1.Should().BeNull(); result2.Should().BeFalse(); higherLevelPath2.Should().Be("some.path"); } [Fact] public void Should_Push_And_ReturnFalse_And_PreviousPath_When_SameScopeId_And_SameObject_And_MorePushedInBetween() { var referencesStack = new ReferencesStack(); var model = new object(); var result1 = referencesStack.TryPush(10, "some.path", model, out var higherLevelPath1); var result2 = referencesStack.TryPush(20, "some.path.next", new object(), out var higherLevelPath2); var result3 = referencesStack.TryPush(30, "some.path.next.next", new object(), out var higherLevelPath3); var result4 = referencesStack.TryPush(10, "some.path.next.next.next", model, out var higherLevelPath4); result1.Should().BeTrue(); higherLevelPath1.Should().BeNull(); result2.Should().BeTrue(); higherLevelPath2.Should().BeNull(); result3.Should().BeTrue(); higherLevelPath3.Should().BeNull(); result4.Should().BeFalse(); higherLevelPath4.Should().Be("some.path"); } [Fact] public void Should_Push_And_ReturnFalse_And_PreviousPath_When_SameScopeId_And_SameObject_And_MorePushedInBetween_AlsoWithSameParameters() { var referencesStack = new ReferencesStack(); var model = new object(); var result1 = referencesStack.TryPush(10, "some.path", model, out var higherLevelPath1); var result2 = referencesStack.TryPush(10, "some.path.next", new object(), out var higherLevelPath2); var result3 = referencesStack.TryPush(20, "some.path.next.next", model, out var higherLevelPath3); var result4 = referencesStack.TryPush(10, "some.path.next.next.next", model, out var higherLevelPath4); result1.Should().BeTrue(); higherLevelPath1.Should().BeNull(); result2.Should().BeTrue(); higherLevelPath2.Should().BeNull(); result3.Should().BeTrue(); higherLevelPath3.Should().BeNull(); result4.Should().BeFalse(); higherLevelPath4.Should().Be("some.path"); } } public class Pop { [Fact] public void Should_ThrowException_When_Empty() { var referencesStack = new ReferencesStack(); Action action = () => referencesStack.Pop(10, out _); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_InvalidKey() { var referencesStack = new ReferencesStack(); referencesStack.TryPush(11, "some", new object(), out _); referencesStack.TryPush(66, "some.path", new object(), out _); Action action = () => referencesStack.Pop(10, out _); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Pop_FromEmptyStack() { var referencesStack = new ReferencesStack(); referencesStack.TryPush(10, "some", new object(), out _); _ = referencesStack.Pop(10, out _); Action action = () => referencesStack.Pop(10, out _); action.Should().ThrowExactly(); } [Fact] public void Should_ThrowException_When_Pop_FromEmptyStack_AfterMultipleOperations() { var referencesStack = new ReferencesStack(); referencesStack.TryPush(10, "some", new object(), out _); referencesStack.TryPush(10, "some", new object(), out _); referencesStack.TryPush(10, "some", new object(), out _); _ = referencesStack.Pop(10, out _); _ = referencesStack.Pop(10, out _); _ = referencesStack.Pop(10, out _); Action action = () => referencesStack.Pop(10, out _); action.Should().ThrowExactly(); } [Fact] public void Should_Pop() { var referencesStack = new ReferencesStack(); var model = new object(); referencesStack.TryPush(10, "some", model, out _); var poppedModel = referencesStack.Pop(10, out var path); poppedModel.Should().BeSameAs(model); path.Should().Be("some"); } [Fact] public void Should_Pop_FromScopeId() { var referencesStack = new ReferencesStack(); var model = new object(); referencesStack.TryPush(10, "some", model, out _); referencesStack.TryPush(11, "some", new object(), out _); var poppedModel = referencesStack.Pop(10, out var path); poppedModel.Should().BeSameAs(model); path.Should().Be("some"); } [Fact] public void Should_Pop_FromScopeId_AfterMultipleOperations() { var referencesStack = new ReferencesStack(); var model1 = new object(); var model2 = new object(); var model3 = new object(); referencesStack.TryPush(10, "some", model1, out _); referencesStack.TryPush(10, "some.path", model2, out _); referencesStack.TryPush(10, "some.path.next", model3, out _); var poppedModel3 = referencesStack.Pop(10, out var path3); var poppedModel2 = referencesStack.Pop(10, out var path2); var poppedModel1 = referencesStack.Pop(10, out var path1); poppedModel1.Should().BeSameAs(model1); path1.Should().Be("some"); poppedModel2.Should().BeSameAs(model2); path2.Should().Be("some.path"); poppedModel3.Should().BeSameAs(model3); path3.Should().Be("some.path.next"); } [Fact] public void Should_Pop_LastSuccessfulPushed() { var referencesStack = new ReferencesStack(); var model1 = new object(); var model2 = new object(); referencesStack.TryPush(10, "some", model1, out _); referencesStack.TryPush(10, "some.path", model2, out _); referencesStack.TryPush(10, "some.path.next", model1, out _); var poppedModel2 = referencesStack.Pop(10, out var path2); var poppedModel1 = referencesStack.Pop(10, out var path1); Action action = () => referencesStack.Pop(10, out _); action.Should().ThrowExactly(); poppedModel1.Should().BeSameAs(model1); path1.Should().Be("some"); poppedModel2.Should().BeSameAs(model2); path2.Should().Be("some.path"); } } public class GetStoredReferencesCount { [Fact] public void Should_BeZero_When_Empty() { var referencesStack = new ReferencesStack(); referencesStack.GetStoredReferencesCount().Should().Be(0); } [Fact] public void Should_BeOne_When_SingleReferenceAdded() { var referencesStack = new ReferencesStack(); referencesStack.TryPush(10, "some.path", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(1); } [Fact] public void Should_Increment_When_PushingItems() { var referencesStack = new ReferencesStack(); referencesStack.TryPush(10, "some.0", new object(), out _); referencesStack.TryPush(11, "some.1", new object(), out _); referencesStack.TryPush(12, "some.2", new object(), out _); referencesStack.TryPush(13, "some.3", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(4); } [Fact] public void Should_Increment_When_TryPushReturnsTrue() { var referencesStack = new ReferencesStack(); var model = new object(); referencesStack.GetStoredReferencesCount().Should().Be(0); referencesStack.TryPush(10, "some.0", model, out _); referencesStack.GetStoredReferencesCount().Should().Be(1); referencesStack.TryPush(11, "some.1", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(2); referencesStack.TryPush(12, "some.2", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(3); referencesStack.TryPush(13, "some.3", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(4); referencesStack.TryPush(10, "some.4", model, out _); referencesStack.GetStoredReferencesCount().Should().Be(4); referencesStack.TryPush(10, "some.5", model, out _); referencesStack.GetStoredReferencesCount().Should().Be(4); referencesStack.TryPush(14, "some.6", model, out _); referencesStack.GetStoredReferencesCount().Should().Be(5); referencesStack.TryPush(15, "some.7", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(6); } [Fact] public void Should_Decrement_When_PoppingItems() { var referencesStack = new ReferencesStack(); referencesStack.TryPush(10, "some.0", new object(), out _); referencesStack.TryPush(11, "some.1", new object(), out _); referencesStack.TryPush(12, "some.2", new object(), out _); referencesStack.TryPush(13, "some.3", new object(), out _); referencesStack.GetStoredReferencesCount().Should().Be(4); referencesStack.Pop(10, out _); referencesStack.GetStoredReferencesCount().Should().Be(3); referencesStack.Pop(11, out _); referencesStack.GetStoredReferencesCount().Should().Be(2); referencesStack.Pop(12, out _); referencesStack.GetStoredReferencesCount().Should().Be(1); referencesStack.Pop(13, out _); referencesStack.GetStoredReferencesCount().Should().Be(0); } } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/TraversingTestCases.cs ================================================ namespace Validot.Tests.Unit.Validation { using System; using System.Collections.Generic; public class TraversingTestCases { public class LoopClassA { private readonly string _id = "A_" + Guid.NewGuid().ToString().Substring(0, 5); public LoopClassA A { get; set; } public LoopClassB B { get; set; } } public class LoopClassB { private readonly string _id = "B_" + Guid.NewGuid().ToString().Substring(0, 5); public LoopClassA[] CollectionA { get; set; } public LoopClassA A { get; set; } public LoopClassB B { get; set; } public LoopClassC C { get; set; } public LoopClassC FieldC; public LoopStructD D; public LoopStructD? NullableD; public LoopStructD[] CollectionD { get; set; } public LoopStructD?[] CollectionNullableD { get; set; } } public class LoopClassC : LoopClassA { private readonly string _id = "C_" + Guid.NewGuid().ToString().Substring(0, 5); public LoopClassC C { get; set; } } public struct LoopStructD { public LoopClassA A { get; set; } public LoopStructE E { get; set; } public LoopStructE? NullableE { get; set; } } public struct LoopStructE { public LoopClassA A { get; set; } } public static IEnumerable Loop_Self() { Specification specification = null; specification = c => c.AsModel(specification); var model1 = new LoopClassA(); model1.A = model1; yield return new object[] { "self", specification, model1, "", "", typeof(LoopClassA) }; } public static IEnumerable Loop_Simple() { Specification specificationA = null; Specification specificationB = null; Specification specificationC = null; specificationA = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB); specificationB = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB) .Member(m => m.C, specificationC); specificationC = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB) .Member(m => m.C, specificationC); var model1 = new LoopClassA() { B = new LoopClassB() }; model1.B.A = model1; yield return new object[] { "simple", specificationA, model1, "", "B.A", typeof(LoopClassA) }; var model2 = new LoopClassA() { A = new LoopClassA() { A = new LoopClassA() { A = new LoopClassA(), B = new LoopClassB() }, B = new LoopClassB() { A = new LoopClassA(), B = new LoopClassB() } }, B = new LoopClassB() }; model2.B.A = model2; yield return new object[] { "simple_withSides", specificationA, model2, "", "B.A", typeof(LoopClassA) }; var model3 = new LoopClassA() { A = new LoopClassA() { A = new LoopClassA() { A = new LoopClassA(), B = new LoopClassB() }, B = new LoopClassB() { A = new LoopClassA(), B = new LoopClassB() } } }; model3.A.B.A.B = model3.A.B; yield return new object[] { "simple_nested", specificationA, model3, "A.B", "A.B.A.B", typeof(LoopClassB) }; var model4 = new LoopClassA() { B = new LoopClassB() { C = new LoopClassC() { A = new LoopClassA() { B = new LoopClassB() { C = new LoopClassC() { A = new LoopClassA() { B = new LoopClassB() } } } } } } }; model4.B.C.A.B.C.A.B.C = model4.B.C; yield return new object[] { "simple_manyClasses", specificationA, model4, "B.C", "B.C.A.B.C.A.B.C", typeof(LoopClassC) }; } public static IEnumerable Loop_ThroughMembers() { Specification specificationA = null; Specification specificationB = null; Specification specificationC = null; Specification specificationD = null; specificationA = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB); specificationB = c => c .Optional() .Member(m => m.FieldC, specificationC) .Member(m => m.D, specificationD) .Member(m => m.B, specificationB); specificationC = c => c .Optional() .Member(m => m.B, specificationB); specificationD = c => c .Member(m => m.A, specificationA); var model1 = new LoopClassA(); model1.A = model1; yield return new object[] { "self_member", specificationA, model1, "", "A", typeof(LoopClassA) }; var model2 = new LoopClassA { A = new LoopClassA() { A = new LoopClassA() { A = new LoopClassA() } } }; model2.A.A.A = model2.A; yield return new object[] { "self_memberNested", specificationA, model2, "A", "A.A.A", typeof(LoopClassA) }; var model3 = new LoopClassA { B = new LoopClassB() { D = new LoopStructD() } }; model3.B.D.A = model3; yield return new object[] { "self_fieldStruct", specificationA, model3, "", "B.D.A", typeof(LoopClassA) }; var model4 = new LoopClassA { B = new LoopClassB() { FieldC = new LoopClassC() } }; model4.B.FieldC.B = model4.B; yield return new object[] { "self_fieldClass", specificationA, model4, "B", "B.FieldC.B", typeof(LoopClassB) }; } public static IEnumerable Loop_ThroughTypes() { Specification specificationA = null; Specification specificationB = null; Specification specificationC = null; Specification specificationD = null; Specification specificationE = null; specificationA = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB); specificationB = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB) .Member(m => m.CollectionA, m => m.Optional().AsCollection(specificationA)) .Member(m => m.C, specificationC) .Member(m => m.FieldC, specificationC) .Member(m => m.D, specificationD) .Member(m => m.CollectionD, m => m.Optional().AsCollection(specificationD)) .Member(m => m.NullableD, m => m.Optional().AsNullable(specificationD)) .Member(m => m.CollectionNullableD, m => m.Optional().AsCollection(m1 => m1.Optional().AsNullable(specificationD))); specificationC = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB) .Member(m => m.C, specificationC); specificationD = c => c .Member(m => m.A, specificationA) .Member(m => m.E, specificationE) .Member(m => m.NullableE, m => m.Optional().AsNullable(specificationE)); specificationE = c => c .Member(m => m.A, specificationA); var model1 = new LoopClassA() { B = new LoopClassB() }; model1.B.A = model1; yield return new object[] { "types_class", specificationA, model1, "", "B.A", typeof(LoopClassA) }; var model2 = new LoopClassA() { B = new LoopClassB() { D = new LoopStructD() } }; model2.B.D.A = model2; yield return new object[] { "types_struct", specificationA, model2, "", "B.D.A", typeof(LoopClassA) }; var model3 = new LoopClassA() { B = new LoopClassB() { NullableD = new LoopStructD() { A = new LoopClassA() } } }; model3.B.NullableD.Value.A.B = model3.B; yield return new object[] { "types_nullable", specificationA, model3, "B", "B.NullableD.A.B", typeof(LoopClassB) }; var model4 = new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA(), null, new LoopClassA(), } } }; model4.B.CollectionA[2] = model4; yield return new object[] { "types_collection", specificationA, model4, "", "B.CollectionA.#2", typeof(LoopClassA) }; var model5 = new LoopClassA() { B = new LoopClassB() { CollectionD = new[] { new LoopStructD(), new LoopStructD(), new LoopStructD(), new LoopStructD(), new LoopStructD() { A = new LoopClassA() } }, } }; model5.B.CollectionD[4].A.B = model5.B; yield return new object[] { "types_collection_structs", specificationA, model5, "B", "B.CollectionD.#4.A.B", typeof(LoopClassB) }; var model6 = new LoopClassA() { B = new LoopClassB() { CollectionNullableD = new LoopStructD?[] { new LoopStructD() { A = new LoopClassA() } }, } }; model6.B.CollectionNullableD[0].Value.A.B = model6.B; yield return new object[] { "types_collection_nullables", specificationA, model6, "B", "B.CollectionNullableD.#0.A.B", typeof(LoopClassB) }; } public static IEnumerable Loop_ThroughIndexes() { Specification specificationA = null; Specification specificationB = null; specificationA = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB); specificationB = c => c .Optional() .Member(m => m.A, specificationA) .Member(m => m.B, specificationB) .Member(m => m.CollectionA, m => m.AsCollection(specificationA)); var model1 = new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA() { B = new LoopClassB() { } } } } }; model1.B.CollectionA[1].B.A = model1.B.CollectionA[1]; yield return new object[] { "indexes_same_amount", specificationA, model1, "B.CollectionA.#1", "B.CollectionA.#1.B.A", typeof(LoopClassA) }; var model2 = new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA() { B = new LoopClassB() { } } } } } } } }; model2.B.CollectionA[1].B.CollectionA[0].B.A = model2.B.CollectionA[1].B.CollectionA[0]; yield return new object[] { "indexes_same_amount_nested", specificationA, model2, "B.CollectionA.#1.B.CollectionA.#0", "B.CollectionA.#1.B.CollectionA.#0.B.A", typeof(LoopClassA) }; var model3 = new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA() { B = new LoopClassB() { A = new LoopClassA() } } } } } } } }; model3.B.CollectionA[1].B.CollectionA[0].B.A.B = model3.B.CollectionA[1].B.CollectionA[0].B; yield return new object[] { "indexes_same_amount_nested_member", specificationA, model3, "B.CollectionA.#1.B.CollectionA.#0.B", "B.CollectionA.#1.B.CollectionA.#0.B.A.B", typeof(LoopClassB) }; var model4 = new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA() { B = new LoopClassB() { A = new LoopClassA() } } } } } } } }; model4.B.CollectionA[1].B.CollectionA[0].B.A.B = model4.B.CollectionA[1].B; yield return new object[] { "indexes_one_between", specificationA, model4, "B.CollectionA.#1.B", "B.CollectionA.#1.B.CollectionA.#0.B.A.B", typeof(LoopClassB) }; var model5 = new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA() { B = new LoopClassB() { CollectionA = new[] { new LoopClassA(), new LoopClassA(), new LoopClassA() { B = new LoopClassB() { A = new LoopClassA() } } } } } } } } } } }; model5.B.CollectionA[1].B.CollectionA[0].B.CollectionA[2].B = model5.B.CollectionA[1].B; yield return new object[] { "indexes_two_between", specificationA, model5, "B.CollectionA.#1.B", "B.CollectionA.#1.B.CollectionA.#0.B.CollectionA.#2.B", typeof(LoopClassB) }; } public class TestClassA { public TestClassA[] CollectionA { get; set; } public TestClassB[] CollectionB { get; set; } public TestClassA A { get; set; } public TestClassB B { get; set; } public TestStructC C { get; set; } } public class TestClassB { public TestClassA A { get; set; } public TestClassB B { get; set; } public TestStructC C { get; set; } public TestStructC? NullableC { get; set; } } public struct TestStructC { public TestClassA A { get; set; } } public static IEnumerable TreesExamples_Common() { Specification specificationB = null; Specification specificationA = s => s .Member(m => m.A, m => m .AsModel(m1 => m1.Rule(x => false)) .Member(m1 => m1.B, m1 => m1.AsModel(m2 => m2.Rule(x => false)))) .Member(m => m.B, specificationB); specificationB = s => s.Member(m => m.A, specificationA); var model1 = new TestClassA() { A = new TestClassA() { B = new TestClassB() } }; var model2 = new TestClassA() { A = new TestClassA() { B = new TestClassB() }, B = new TestClassB() { A = new TestClassA(), B = new TestClassB(), C = new TestStructC(), }, C = new TestStructC(), }; var model3 = new TestClassA() { A = new TestClassA() { B = new TestClassB() }, B = new TestClassB() { A = new TestClassA() { A = new TestClassA() { B = new TestClassB() }, B = new TestClassB() { A = new TestClassA(), B = new TestClassB(), C = new TestStructC(), }, C = new TestStructC() { A = new TestClassA() { A = new TestClassA() { B = new TestClassB() }, B = new TestClassB() { A = new TestClassA(), B = new TestClassB(), C = new TestStructC(), }, C = new TestStructC() } } }, B = new TestClassB(), C = new TestStructC(), }, C = new TestStructC() { A = new TestClassA() { A = new TestClassA() { B = new TestClassB() }, B = new TestClassB() { A = new TestClassA(), B = new TestClassB(), C = new TestStructC(), }, C = new TestStructC() } } }; yield return new object[] { "common1", specificationA, model1 }; yield return new object[] { "common2", specificationA, model2 }; yield return new object[] { "common3", specificationA, model3 }; } public static IEnumerable TreesExamples_Struct() { Specification specificationA = null; Specification specificationB = null; Specification specificationC = null; specificationA = s => s.Member(m => m.B, specificationB); specificationB = s => s.Member(m => m.C, specificationC); specificationC = s => s.Member(m => m.A, specificationA); var model1 = new TestClassA() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() } }, } } }, } } }, }; var model2 = new TestClassA() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() } }, B = new TestClassB() { C = new TestStructC() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() { A = new TestClassA() { B = new TestClassB() { C = new TestStructC() } }, } } }, } } }, } }, C = new TestStructC() { A = new TestClassA() { B = new TestClassB() } } }; yield return new object[] { "struct1", specificationA, model1 }; yield return new object[] { "struct2", specificationA, model2 }; } public static IEnumerable TreesExamples_Collections() { Specification specificationA = null; Specification specificationB = null; specificationA = s => s .Optional() .Member(m => m.CollectionA, m => m.AsCollection(specificationA)) .Member(m => m.CollectionB, m => m.AsCollection(specificationB)); specificationB = s => s .Optional() .Member(m => m.A, specificationA); var model31 = new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), } }; var model32 = new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), }, CollectionB = new[] { new TestClassB(), new TestClassB(), new TestClassB(), } }; var model33 = new TestClassA() { CollectionA = new[] { new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), }, CollectionB = new[] { new TestClassB(), new TestClassB(), new TestClassB(), } }, new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), }, CollectionB = new[] { new TestClassB(), new TestClassB(), new TestClassB(), } }, }, CollectionB = new[] { new TestClassB() { A = new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), }, CollectionB = new[] { new TestClassB(), new TestClassB(), new TestClassB(), } }, }, new TestClassB() { A = new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), }, CollectionB = new[] { new TestClassB() { A = new TestClassA() { CollectionA = new[] { new TestClassA(), new TestClassA(), new TestClassA(), }, CollectionB = new[] { new TestClassB(), new TestClassB(), new TestClassB(), } }, }, new TestClassB(), new TestClassB(), } }, }, } }; yield return new object[] { "collection1", specificationA, model31 }; yield return new object[] { "collection2", specificationA, model32 }; yield return new object[] { "collection3", specificationA, model33 }; } public static IEnumerable TreesExamples_Nullable() { Specification specificationA = null; Specification specificationB = null; Specification specificationC = null; specificationA = s => s .Optional() .Member(m => m.B, specificationB); specificationB = s => s .Optional() .Member(m => m.C, specificationC) .Member(m => m.NullableC, m => m.AsNullable(specificationC)); specificationC = s => s .Member(m => m.A, specificationA); var model1 = new TestClassA() { B = new TestClassB() { NullableC = new TestStructC() { A = new TestClassA() }, C = new TestStructC() { A = new TestClassA() } } }; var model2 = new TestClassA() { B = new TestClassB() { NullableC = new TestStructC() { A = new TestClassA() { B = new TestClassB() { NullableC = new TestStructC() { A = new TestClassA() }, C = new TestStructC() { A = new TestClassA() } } }, }, C = new TestStructC() { A = new TestClassA() { B = new TestClassB() { NullableC = new TestStructC() { A = new TestClassA() }, C = new TestStructC() { A = new TestClassA() } } } } } }; yield return new object[] { "nullable1", specificationA, model1 }; yield return new object[] { "nullable2", specificationA, model2 }; } } } ================================================ FILE: tests/Validot.Tests.Unit/Validation/ValidationContextTests.cs ================================================ namespace Validot.Tests.Unit.Validation { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using NSubstitute; using NSubstitute.ExceptionExtensions; using Validot.Validation; using Validot.Validation.Scheme; using Validot.Validation.Scopes; using Validot.Validation.Scopes.Builders; using Validot.Validation.Stacks; using Xunit; public class ValidationContextTests { public class Initializing { [Fact] public void Should_Initialize() { var modelScheme = Substitute.For(); _ = new ValidationContext(modelScheme, default, default); } [Fact] public void Should_Initialize_WithDefaultValues() { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, default, default); validationContext.FailFast.Should().BeFalse(); validationContext.ReferenceLoopProtectionSettings.Should().BeNull(); validationContext.Errors.Should().BeNull(); validationContext.ShouldFallBack.Should().BeFalse(); } [Theory] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(false, false)] public void Should_Initialize_WithValues(bool failFast, bool passLoopProtectionSettings) { var modelScheme = Substitute.For(); ReferenceLoopProtectionSettings referenceLoopProtectionSettings = null; if (passLoopProtectionSettings) { modelScheme.RootModelType.Returns(typeof(object)); referenceLoopProtectionSettings = new ReferenceLoopProtectionSettings(); } var validationContext = new ValidationContext(modelScheme, failFast, referenceLoopProtectionSettings); validationContext.FailFast.Should().Be(failFast); if (passLoopProtectionSettings) { validationContext.ReferenceLoopProtectionSettings.Should().BeSameAs(referenceLoopProtectionSettings); } } [Theory] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(false, false)] public void Should_Initialize_ReferencesStack_With_Null_When_LoopProtectionSettings_Is_Null(bool failFast, bool rootModelTypeIsReference) { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(rootModelTypeIsReference ? typeof(object) : typeof(int)); var validationContext = new ValidationContext( modelScheme, failFast, null); validationContext.GetLoopProtectionReferencesStackCount().Should().BeNull(); } [Theory] [InlineData(true, true)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(false, false)] public void Should_Initialize_ReferencesStack_With_Zero_When_LoopProtectionSettings_Has_NullRootModel(bool failFast, bool rootModelTypeIsReference) { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(rootModelTypeIsReference ? typeof(object) : typeof(int)); var validationContext = new ValidationContext( modelScheme, failFast, new ReferenceLoopProtectionSettings()); validationContext.GetLoopProtectionReferencesStackCount().Should().Be(0); } [Theory] [InlineData(true)] [InlineData(false)] public void Should_Initialize_ReferencesStack_With_One_When_LoopProtectionSettings_Has_RootModel_And_RootModelTypeInSchemeIsReferenceType(bool failFast) { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(typeof(object)); var validationContext = new ValidationContext( modelScheme, failFast, new ReferenceLoopProtectionSettings(new object())); validationContext.GetLoopProtectionReferencesStackCount().Should().Be(1); } [Theory] [InlineData(true)] [InlineData(false)] public void Should_Initialize_ReferencesStack_With_Zero_When_LoopProtectionSettings_Has_RootModel_And_RootModelTypeInSchemeIsValueType(bool failFast) { var modelScheme = Substitute.For(); modelScheme.RootModelType.Returns(typeof(int)); var validationContext = new ValidationContext( modelScheme, failFast, new ReferenceLoopProtectionSettings(new object())); validationContext.GetLoopProtectionReferencesStackCount().Should().Be(0); } } public class AddError { [Fact] public void Should_AddError_ToDefaultPath() { var modelScheme = Substitute.For(); var context = new ValidationContext(modelScheme, default, default); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors[string.Empty].Should().HaveCount(1); context.Errors[string.Empty].ElementAt(0).Should().Be(123); } [Fact] public void Should_AddErrors_ToDefaultPath() { var modelScheme = Substitute.For(); var context = new ValidationContext(modelScheme, default, default); context.AddError(123); context.AddError(321); context.AddError(666); context.Errors.Should().HaveCount(1); context.Errors[string.Empty].Should().HaveCount(3); context.Errors[string.Empty].Should().ContainInOrder(123, 321, 666); } [Fact] public void Should_AddError_ToEnteredPath() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(123); } [Fact] public void Should_AddErrors_ToEnteredPath() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(123); context.AddError(321); context.AddError(666); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(3); context.Errors["entered.path"].Should().ContainInOrder(123, 321, 666); } [Fact] public void Should_AddError_When_AlreadyExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_False() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(123); context.AddError(123); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(3); context.Errors["entered.path"].Should().ContainInOrder(123, 123, 123); } [Fact] public void Should_NotAddError_When_AlreadyExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_True() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(123, true); context.AddError(123, true); context.AddError(123, true); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(123); } [Fact] public void Should_AddErrors_When_NotExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_True() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(123, true); context.AddError(321, true); context.AddError(666, true); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(3); context.Errors["entered.path"].Should().ContainInOrder(123, 321, 666); } [Fact] public void Should_AddError_OnlyWhen_NotExistsUnderSamePath_And_SkipIfDuplicateInPath_Is_True() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("test1")).Returns("test1"); modelScheme.ResolvePath(Arg.Is("test1"), Arg.Is("test2")).Returns("test1.test2"); modelScheme.ResolvePath(Arg.Is("test1.test2"), Arg.Is("test3")).Returns("test1.test2.test3"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("test1"); context.AddError(123, true); context.AddError(123, true); context.AddError(123, false); context.EnterPath("test2"); context.AddError(123, true); context.AddError(123, true); context.AddError(123, false); context.EnterPath("test3"); context.AddError(123, true); context.AddError(123, false); context.AddError(123, false); context.Errors.Should().HaveCount(3); context.Errors["test1"].Should().HaveCount(2); context.Errors["test1"].Should().ContainInOrder(123, 123); context.Errors["test1.test2"].Should().HaveCount(2); context.Errors["test1.test2"].Should().ContainInOrder(123, 123); context.Errors["test1.test2.test3"].Should().HaveCount(3); context.Errors["test1.test2.test3"].Should().ContainInOrder(123, 123, 123); } } public class EnterPath_And_AddingErrors { [Theory] [InlineData("some.path")] [InlineData("")] [InlineData("#2.#123.path.#321")] public void AddErrors_Should_AddToEnteredPath_AfterStepIntoNextPath_ResolvedByModelScheme(string pathForScope) { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entry.path")).Returns(pathForScope); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entry.path"); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors[pathForScope].Should().HaveCount(1); context.Errors[pathForScope].Should().ContainInOrder(123); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entry.path")); } [Fact] public void AddErrors_Should_AddToSamePath_AfterEnteredPathIsNull() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entry.path")).Returns("entry.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entry.path"); context.EnterPath(null); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entry.path"].Should().HaveCount(1); context.Errors["entry.path"].Should().ContainInOrder(123); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entry.path")); modelScheme.DidNotReceive().ResolvePath(Arg.Is(""), Arg.Is((string)null)); } [Fact] public void AddErrors_Should_AddToEnteredPath_AfterStepIntoNextPath() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entry.path")).Returns("entry.path"); modelScheme.ResolvePath(Arg.Is("entry.path"), Arg.Is("next")).Returns("entry.path.next"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entry.path"); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entry.path"].Should().HaveCount(1); context.Errors["entry.path"].Should().ContainInOrder(123); context.EnterPath("next"); context.AddError(321); context.Errors.Should().HaveCount(2); context.Errors["entry.path"].Should().HaveCount(1); context.Errors["entry.path"].Should().ContainInOrder(123); context.Errors["entry.path.next"].Should().HaveCount(1); context.Errors["entry.path.next"].Should().ContainInOrder(321); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entry.path")); modelScheme.Received(1).ResolvePath(Arg.Is("entry.path"), Arg.Is("next")); } [Fact] public void AddErrors_Should_AddToPreviousPathAfterStepOut() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entry.path")).Returns("entry.path"); modelScheme.ResolvePath(Arg.Is("entry.path"), Arg.Is("next")).Returns("entry.path.next"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entry.path"); context.EnterPath("next"); context.LeavePath(); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entry.path"].Should().HaveCount(1); context.Errors["entry.path"].Should().ContainInOrder(123); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entry.path")); modelScheme.Received(1).ResolvePath(Arg.Is("entry.path"), Arg.Is("next")); } [Fact] public void AddErrors_Should_AddToEnteredPath_And_ToPreviousPathAfterStepOut() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entry.path")).Returns("entry.path"); modelScheme.ResolvePath(Arg.Is("entry.path"), Arg.Is("next")).Returns("entry.path.next"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entry.path"); context.EnterPath("next"); context.AddError(321); context.Errors.Should().HaveCount(1); context.Errors["entry.path.next"].Should().HaveCount(1); context.Errors["entry.path.next"].Should().ContainInOrder(321); context.LeavePath(); context.AddError(123); context.Errors.Should().HaveCount(2); context.Errors["entry.path"].Should().HaveCount(1); context.Errors["entry.path"].Should().ContainInOrder(123); context.Errors["entry.path.next"].Should().HaveCount(1); context.Errors["entry.path.next"].Should().ContainInOrder(321); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entry.path")); modelScheme.Received(1).ResolvePath(Arg.Is("entry.path"), Arg.Is("next")); } [Fact] public void Should_AddError_ToEnteredCollectionItemPath() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("#")).Returns($"#"); modelScheme.GetPathWithIndexes(Arg.Is("#"), Arg.Is>(a => a.Single() == "666")).Returns("#666"); var context = new ValidationContext(modelScheme, default, default); context.EnterCollectionItemPath(666); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["#666"].Should().HaveCount(1); context.Errors["#666"].Should().ContainInOrder(123); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("#")); modelScheme.Received(1).GetPathWithIndexes(Arg.Is("#"), Arg.Is>(a => a.Single() == "666")); } [Fact] public void Should_AddError_ToEnteredCollectionItemPath_InSecondLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("#")).Returns($"entered.path.#"); modelScheme.GetPathWithIndexes(Arg.Is("entered.path.#"), Arg.Is>(a => a.Single() == "666")).Returns("entered.path.#666"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnterCollectionItemPath(666); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entered.path.#666"].Should().HaveCount(1); context.Errors["entered.path.#666"].Should().ContainInOrder(123); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entered.path")); modelScheme.Received(1).ResolvePath(Arg.Is("entered.path"), Arg.Is("#")); modelScheme.Received(1).GetPathWithIndexes(Arg.Is("entered.path.#"), Arg.Is>(a => a.Single() == "666")); } [Fact] public void Should_AddError_AddToPreviousPath_After_LeavingEnteredCollectionItemPath() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("#")).Returns($"entered.path.#666"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnterCollectionItemPath(666); context.LeavePath(); context.AddError(123); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(123); modelScheme.Received(1).ResolvePath(Arg.Is(""), Arg.Is("entered.path")); modelScheme.Received(1).ResolvePath(Arg.Is("entered.path"), Arg.Is("#")); } } public class EnterScope { public class TestClass { } [Fact] public void Should_UseModelScheme() { var specificationScope = Substitute.For>(); var modelScheme = Substitute.For(); modelScheme.GetSpecificationScope(Arg.Is(1234)).Returns(specificationScope); var context = new ValidationContext(modelScheme, default, default); var model = new TestClass(); context.EnterScope(1234, model); Received.InOrder(() => { modelScheme.GetSpecificationScope(Arg.Is(1234)); specificationScope.Validate(Arg.Is(model), Arg.Is(context)); }); specificationScope.DidNotReceive().Discover(Arg.Any()); specificationScope.Received(1).Validate(Arg.Any(), Arg.Any()); } [Fact] public void Should_UseModelScheme_MultipleTimes() { var specificationScope = Substitute.For>(); var modelScheme = Substitute.For(); modelScheme.GetSpecificationScope(Arg.Is(1234)).Returns(specificationScope); var context = new ValidationContext(modelScheme, default, default); var model = new TestClass(); context.EnterScope(1234, model); context.EnterScope(1234, model); Received.InOrder(() => { modelScheme.GetSpecificationScope(Arg.Is(1234)); specificationScope.Validate(Arg.Is(model), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(1234)); specificationScope.Validate(Arg.Is(model), Arg.Is(context)); }); specificationScope.DidNotReceive().Discover(Arg.Any()); specificationScope.Received(2).Validate(Arg.Any(), Arg.Any()); } [Fact] public void Should_Return_DifferentScopesForDifferentTypesAndIds() { var model1 = new TestClass(); var specificationScope1 = Substitute.For>(); var model2 = new TestClass(); var specificationScope2 = Substitute.For>(); var model3 = new DateTimeOffset?(DateTimeOffset.FromUnixTimeSeconds(3)); var specificationScope3 = Substitute.For>(); var model4 = 4M; var specificationScope4 = Substitute.For>(); var modelScheme = Substitute.For(); modelScheme.GetSpecificationScope(Arg.Is(1)).Returns(specificationScope1); modelScheme.GetSpecificationScope(Arg.Is(2)).Returns(specificationScope2); modelScheme.GetSpecificationScope(Arg.Is(3)).Returns(specificationScope3); modelScheme.GetSpecificationScope(Arg.Is(4)).Returns(specificationScope4); var context = new ValidationContext(modelScheme, default, default); context.EnterScope(1, model1); context.EnterScope(2, model2); context.EnterScope(3, model3); context.EnterScope(4, model4); Received.InOrder(() => { modelScheme.GetSpecificationScope(Arg.Is(1)); specificationScope1.Validate(Arg.Is(model1), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(2)); specificationScope2.Validate(Arg.Is(model2), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(3)); specificationScope3.Validate(Arg.Is(model3), Arg.Is(context)); modelScheme.GetSpecificationScope(Arg.Is(4)); specificationScope4.Validate(Arg.Is(model4), Arg.Is(context)); }); modelScheme.Received(1).GetSpecificationScope(Arg.Is(1)); modelScheme.Received(1).GetSpecificationScope(Arg.Is(2)); modelScheme.Received(1).GetSpecificationScope(Arg.Is(3)); modelScheme.Received(1).GetSpecificationScope(Arg.Is(4)); } [Fact] public void Should_RethrowException_When_ModelSchemeThrows() { var modelScheme = Substitute.For(); var exception = new KeyNotFoundException(); modelScheme.GetSpecificationScope(Arg.Is(1234)).Throws(exception); var context = new ValidationContext(modelScheme, default, default); Action action = () => context.EnterScope(1234, new TestClass()); action.Should().ThrowExactly(); } } public class EnterScope_And_Fail_When_ReferenceLoopExists { [Theory] [MemberData(nameof(TraversingTestCases.Loop_Self), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_Simple), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_ThroughMembers), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_ThroughTypes), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.Loop_ThroughIndexes), MemberType = typeof(TraversingTestCases))] public void Should_ThrowException_InfiniteReferencesLoopException_WithDetectedLoopInfo_When_ReferencesLoopDetected(string testId, Specification specification, TraversingTestCases.LoopClassA model, string path, string infiniteLoopNestedPath, Type type) { _ = testId; var modelScheme = ModelSchemeFactory.Create(specification); var context = new ValidationContext(modelScheme, default, new ReferenceLoopProtectionSettings()); Action action = () => context.EnterScope(modelScheme.RootSpecificationScopeId, model); var exception = action.Should().ThrowExactly().And; exception.Path.Should().Be(path); exception.NestedPath.Should().Be(infiniteLoopNestedPath); exception.Type.Should().Be(type); var pathStringified = string.IsNullOrEmpty(path) ? "the root path, so the validated object itself," : $"the path '{path}'"; exception.Message.Should().Be($"Reference loop detected: object of type {type.GetFriendlyName()} has been detected twice in the reference graph, effectively creating an infinite references loop (at first under {pathStringified} and then under the nested path '{infiniteLoopNestedPath}')"); } } public class EnterScope_And_TrackingReferencesLoops { [Theory] [MemberData(nameof(TraversingTestCases.TreesExamples_Common), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Struct), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Collections), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Nullable), MemberType = typeof(TraversingTestCases))] public void Should_GetLoopProtectionReferencesStackCount_BeZero_BeforeAndAfterEnteringRootScope_When_NoRootReferenceInSettings(string id, Specification rootSpecification, TraversingTestCases.TestClassA model) { _ = id; var modelScheme = ModelSchemeFactory.Create(rootSpecification); var context = new ValidationContext(modelScheme, default, new ReferenceLoopProtectionSettings()); context.GetLoopProtectionReferencesStackCount().Should().Be(0); context.EnterScope(modelScheme.RootSpecificationScopeId, model); context.GetLoopProtectionReferencesStackCount().Should().Be(0); } [Theory] [MemberData(nameof(TraversingTestCases.TreesExamples_Common), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Struct), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Collections), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Nullable), MemberType = typeof(TraversingTestCases))] public void Should_GetLoopProtectionReferencesStackCount_BeOne_BeforeAndAfterEnteringRootScope_When_RootModelReference_Exists(string id, Specification rootSpecification, TraversingTestCases.TestClassA model) { _ = id; var modelScheme = ModelSchemeFactory.Create(rootSpecification); var context = new ValidationContext(modelScheme, default, new ReferenceLoopProtectionSettings(new object())); context.GetLoopProtectionReferencesStackCount().Should().Be(1); context.EnterScope(modelScheme.RootSpecificationScopeId, model); context.GetLoopProtectionReferencesStackCount().Should().Be(1); } [Theory] [MemberData(nameof(TraversingTestCases.TreesExamples_Common), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Struct), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Collections), MemberType = typeof(TraversingTestCases))] [MemberData(nameof(TraversingTestCases.TreesExamples_Nullable), MemberType = typeof(TraversingTestCases))] public void Should_GetLoopProtectionReferencesStackCount_BeNull_BeforeAndAfterEnteringRootScope_When_RootModelReference_IsNull(string id, Specification rootSpecification, TraversingTestCases.TestClassA model) { _ = id; var modelScheme = ModelSchemeFactory.Create(rootSpecification); var context = new ValidationContext(modelScheme, default, default); context.GetLoopProtectionReferencesStackCount().Should().BeNull(); context.EnterScope(modelScheme.RootSpecificationScopeId, model); context.GetLoopProtectionReferencesStackCount().Should().BeNull(); } } public class ShouldFallback { [Fact] public void Should_BeFalse_If_FailFastIsFalse_And_ErrorDetectionModeIsOff() { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, default, default); validationContext.ShouldFallBack.Should().BeFalse(); } [Fact] public void Should_BeFalse_If_FailFastIsTrue_And_NoErrorsAdded() { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, true, default); validationContext.ShouldFallBack.Should().BeFalse(); } [Fact] public void Should_BeFalse_If_FailFastIsFalse_And_AnyErrorsAdded() { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, false, default); validationContext.AddError(123); validationContext.ShouldFallBack.Should().BeFalse(); } [Fact] public void Should_BeTrue_If_FailFastIsTrue_And_AnyErrorsAdded() { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, true, default); validationContext.AddError(123); validationContext.ShouldFallBack.Should().BeTrue(); } [Theory] [InlineData(true, ErrorMode.Append)] [InlineData(true, ErrorMode.Override)] [InlineData(false, ErrorMode.Append)] [InlineData(false, ErrorMode.Override)] public void Should_BeFalse_When_ErrorModeEnabled(bool failFast, object errorModeBoxed) { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, failFast, default); validationContext.EnableErrorDetectionMode((ErrorMode)errorModeBoxed, 123); validationContext.ShouldFallBack.Should().BeFalse(); } [Theory] [InlineData(true)] [InlineData(false)] public void Should_BeTrue_If_OverridingModeEnabled_And_AnyErrorsAdded(bool failFast) { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, failFast, default); validationContext.EnableErrorDetectionMode(ErrorMode.Override, 321); validationContext.AddError(123); validationContext.ShouldFallBack.Should().BeTrue(); } [Fact] public void Should_BeTrue_When_FailFastIsFalse_OnlyAfter_OverridingModeEnabled() { var modelScheme = Substitute.For(); var validationContext = new ValidationContext(modelScheme, default, default); validationContext.AddError(1); validationContext.ShouldFallBack.Should().BeFalse(); validationContext.EnableErrorDetectionMode(ErrorMode.Override, 321); validationContext.AddError(2); validationContext.ShouldFallBack.Should().BeTrue(); } } public class AppendErrorsMode { [Fact] public void Should_NotAppendErrors_When_AppendErrorModeDisabled_And_LeavingPathWithErrors() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(1); context.AddError(2); context.AddError(3); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(3); context.Errors["entered.path"].Should().ContainInOrder(1, 2, 3); } [Fact] public void Should_NotAppendErrors_When_AppendErrorModeEnabled_And_LeavingPathWithoutErrors() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.LeavePath(); context.Errors.Should().BeNull(); } [Fact] public void Should_NotAppendErrors_When_AppendErrorModeEnabled_And_LeavingPathWithoutErrors_AtManyLevels() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Append, 2); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Append, 3); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().BeNull(); } [Fact] public void Should_NotAppendErrors_When_ModeEnabledAfterErrorIsAdded() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(1); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(1); } [Fact] public void Should_AppendErrors_When_AppendErrorModeEnabled_And_LeavingPathWithError() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(2); context.Errors["entered.path"].Should().ContainInOrder(1, 321); } [Fact] public void Should_AppendErrors_When_AppendErrorModeEnabled_And_LeavingPathWithErrors() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.AddError(2); context.AddError(3); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(4); context.Errors["entered.path"].Should().ContainInOrder(1, 2, 3, 321); } [Fact] public void Should_AppendErrors_ToLevelsWithModeEnabled_When_ErrorsOnEveryLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(2); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(1); context.Errors["entered.path.nested"].Should().HaveCount(2); context.Errors["entered.path.nested"].Should().ContainInOrder(2, 321); } [Fact] public void Should_AppendErrors_ToLevelsWithModeEnabled_When_ModeEnabledAtLowLevel_And_ErrorIsNested() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more"); context.AddError(1); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); context.Errors["entered.path.nested.more"].Should().HaveCount(1); context.Errors["entered.path.nested.more"].Should().ContainInOrder(1); } [Fact] public void Should_AppendErrors_ToLevelsWithModeEnabled_When_ModeEnabledOnDifferentLevels_And_ErrorOnAllLevels() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.EnterPath("entered.path.nested"); context.AddError(2); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Append, 123); context.AddError(3); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(3); context.Errors["entered.path"].Should().HaveCount(2); context.Errors["entered.path"].Should().ContainInOrder(1, 321); context.Errors["entered.path.nested"].Should().HaveCount(1); context.Errors["entered.path.nested"].Should().ContainInOrder(2); context.Errors["entered.path.nested.more"].Should().HaveCount(2); context.Errors["entered.path.nested.more"].Should().ContainInOrder(3, 123); } [Fact] public void Should_AppendErrors_ToLevelsWithModeEnabled_AndErrorsNested_When_ModeEnabledOnAllLevels_And_ErrorOnLowLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Append, 661); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Append, 662); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(2); context.Errors["entered.path"].Should().ContainInOrder(1, 321); } [Fact] public void Should_AppendErrors_ToLevelsWithModeEnabled_AndErrorsNested_When_ModeEnabledOnAllLevels_And_ErrorOnNestedLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 111); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Append, 113); context.AddError(1); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(3); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); context.Errors["entered.path.nested"].Should().HaveCount(1); context.Errors["entered.path.nested"].Should().ContainInOrder(112); context.Errors["entered.path.nested.more"].Should().HaveCount(2); context.Errors["entered.path.nested.more"].Should().ContainInOrder(1, 113); } [Fact] public void Should_AppendErrors_WhenTravelingUpAndDownTheTree_OnlyToEnabledLevelsBelowErrorDetectedAtTheTimeOfDetection_When_HavingNestedBranchWithEnabledLevelWithoutError() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more1")).Returns("entered.path.nested.more1"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more2")).Returns("entered.path.nested.more2"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 111); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more1"); context.AddError(1); context.LeavePath(); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more2"); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); context.Errors["entered.path.nested.more1"].Should().HaveCount(1); context.Errors["entered.path.nested.more1"].Should().ContainInOrder(1); } [Fact] public void Should_AppendErrors_WhenTravelingUpAndDownTheTree_OnlyToEnabledLevelsBelowErrorDetectedAtTheTimeOfDetection_When_HavingNestedBranchWithEnabledLevelWithError() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more1")).Returns("entered.path.nested.more1"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more2")).Returns("entered.path.nested.more2"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 111); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more1"); context.AddError(1); context.LeavePath(); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more2"); context.AddError(2); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(4); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); context.Errors["entered.path.nested.more1"].Should().HaveCount(1); context.Errors["entered.path.nested.more1"].Should().ContainInOrder(1); context.Errors["entered.path.nested.more2"].Should().HaveCount(1); context.Errors["entered.path.nested.more2"].Should().ContainInOrder(2); context.Errors["entered.path.nested"].Should().HaveCount(1); context.Errors["entered.path.nested"].Should().ContainInOrder(112); } [Fact] public void Should_AppendErrors_WhenTravelingUpAndDownTheTree_OnlyToEnabledLevelsBelowErrorDetectedAtTheTimeOfDetection_When_HavingNestedBranchWithEnabledLevelWithError_OnlyHere() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more1")).Returns("entered.path.nested.more1"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more2")).Returns("entered.path.nested.more2"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 111); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more1"); context.LeavePath(); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more2"); context.AddError(2); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(3); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); context.Errors["entered.path.nested.more2"].Should().HaveCount(1); context.Errors["entered.path.nested.more2"].Should().ContainInOrder(2); context.Errors["entered.path.nested"].Should().HaveCount(1); context.Errors["entered.path.nested"].Should().ContainInOrder(112); } [Fact] public void Should_AppendErrorMode_SetDisabled_When_ErrorsAddedAndLevelLeft() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("first")).Returns("first"); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("second")).Returns("second"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("first"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.LeavePath(); context.EnterPath("second"); context.AddError(2); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["first"].Should().HaveCount(2); context.Errors["first"].Should().ContainInOrder(1, 321); context.Errors["second"].Should().HaveCount(1); context.Errors["second"].Should().ContainInOrder(2); } [Fact] public void Should_AppendErrorMode_SetDisabledAndEnableAgain() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("first")).Returns("first"); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("second")).Returns("second"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("first"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.LeavePath(); context.EnterPath("second"); context.EnableErrorDetectionMode(ErrorMode.Append, 123); context.AddError(2); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["first"].Should().HaveCount(2); context.Errors["first"].Should().ContainInOrder(1, 321); context.Errors["second"].Should().HaveCount(2); context.Errors["second"].Should().ContainInOrder(2, 123); } } public class OverrideErrorsMode { [Fact] public void Should_NotOverrideErrors_When_ModeDisabled_And_LeavingPathWithErrors() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(1); context.AddError(2); context.AddError(3); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(3); context.Errors["entered.path"].Should().ContainInOrder(1, 2, 3); } [Fact] public void Should_NotOverrideErrors_When_ModeEnabled_And_LeavingPathWithoutErrors() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.LeavePath(); context.Errors.Should().BeNull(); } [Fact] public void Should_NotOverrideErrors_When_ModeEnabled_And_LeavingPathWithoutErrors_AtManyLevels() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Override, 2); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Override, 3); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().BeNull(); } [Fact] public void Should_NotOverrideErrors_When_ModeEnabledAfterErrorIsAdded() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(1); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(1); } [Fact] public void Should_OverrideErrors_When_ModeEnabled_And_LeavingPathWithError() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); } [Fact] public void Should_OverrideErrors_When_ModeEnabled_And_LeavingPathWithErrors() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.AddError(2); context.AddError(3); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); } [Fact] public void Should_OverrideErrors_OnLevelsWithModeEnabled_When_ErrorsOnEveryLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.AddError(1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(2); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(1); context.Errors["entered.path.nested"].Should().HaveCount(1); context.Errors["entered.path.nested"].Should().ContainInOrder(321); } [Fact] public void Should_OverrideErrors_OnLowestLevelWithModeEnabled_When_ModeEnabledAtLowLevel_And_ErrorIsNested() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more"); context.AddError(1); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); } [Fact] public void Should_OverrideErrors_OnLowestLevelWithModeEnabled_When_ModeEnabledOnDifferentLevels_And_ErrorOnAllLevels() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.EnterPath("entered.path.nested"); context.AddError(2); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Override, 123); context.AddError(3); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); } [Fact] public void Should_OverrideErrors_OnLowestLevelWithModeEnabled_When_ModeEnabledOnAllLevels_And_ErrorOnLowLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Override, 661); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Override, 662); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); } [Fact] public void Should_OverrideErrors_OnLowestLevelWithModeEnabled_When_ModeEnabledOnAllLevels_And_ErrorOnNestedLevel() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 111); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Override, 112); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Override, 113); context.AddError(1); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); } [Fact] public void Should_OverrideErrors_WhenTravelingUpAndDownTheTree_ToRootEnabledLevel_When_HavingNestedBranchWithEnabledLevelWithoutError() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more1")).Returns("entered.path.nested.more1"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more2")).Returns("entered.path.nested.more2"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 111); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more1"); context.AddError(1); context.LeavePath(); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more2"); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); } [Fact] public void Should_OverrideErrors_WhenTravelingUpAndDownTheTree_ToRootEnabledLevel_When_HavingNestedBranchWithEnabledLevelWithError() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more1")).Returns("entered.path.nested.more1"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more2")).Returns("entered.path.nested.more2"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 111); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more1"); context.AddError(1); context.LeavePath(); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more2"); context.AddError(2); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); } [Fact] public void Should_OverrideErrors_WhenTravelingUpAndDownTheTree_ToRootEnabledLevel_When_HavingNestedBranchWithEnabledLevelWithError_OnlyHere() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more1")).Returns("entered.path.nested.more1"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more2")).Returns("entered.path.nested.more2"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 111); context.EnterPath("entered.path.nested"); context.EnterPath("entered.path.nested.more1"); context.LeavePath(); context.EnableErrorDetectionMode(ErrorMode.Append, 112); context.EnterPath("entered.path.nested.more2"); context.AddError(2); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(111); } [Fact] public void Should_Mode_SetDisabled_When_ErrorsAddedAndLevelLeft() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("first")).Returns("first"); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("second")).Returns("second"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("first"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.LeavePath(); context.EnterPath("second"); context.AddError(2); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["first"].Should().HaveCount(1); context.Errors["first"].Should().ContainInOrder(321); context.Errors["second"].Should().HaveCount(1); context.Errors["second"].Should().ContainInOrder(2); } [Fact] public void Should_Mode_SetDisabledAndEnableAgain() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("first")).Returns("first"); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("second")).Returns("second"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("first"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.LeavePath(); context.EnterPath("second"); context.EnableErrorDetectionMode(ErrorMode.Override, 123); context.AddError(2); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["first"].Should().HaveCount(1); context.Errors["first"].Should().ContainInOrder(321); context.Errors["second"].Should().HaveCount(1); context.Errors["second"].Should().ContainInOrder(123); } [Fact] public void Should_Mode_NotBeAffectedByOtherModes() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Override, 321); context.AddError(1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Append, 661); context.AddError(2); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Override, 662); context.AddError(3); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(1); context.Errors["entered.path"].Should().HaveCount(1); context.Errors["entered.path"].Should().ContainInOrder(321); } [Fact] public void Should_Mode_WorkWithinAppendMode() { var modelScheme = Substitute.For(); modelScheme.ResolvePath(Arg.Is(""), Arg.Is("entered.path")).Returns("entered.path"); modelScheme.ResolvePath(Arg.Is("entered.path"), Arg.Is("entered.path.nested")).Returns("entered.path.nested"); modelScheme.ResolvePath(Arg.Is("entered.path.nested"), Arg.Is("entered.path.nested.more")).Returns("entered.path.nested.more"); var context = new ValidationContext(modelScheme, default, default); context.EnterPath("entered.path"); context.EnableErrorDetectionMode(ErrorMode.Append, 321); context.AddError(1); context.EnterPath("entered.path.nested"); context.EnableErrorDetectionMode(ErrorMode.Override, 661); context.AddError(2); context.EnterPath("entered.path.nested.more"); context.EnableErrorDetectionMode(ErrorMode.Override, 662); context.AddError(3); context.LeavePath(); context.LeavePath(); context.LeavePath(); context.Errors.Should().HaveCount(2); context.Errors["entered.path"].Should().HaveCount(2); context.Errors["entered.path"].Should().ContainInOrder(1, 321); context.Errors["entered.path.nested"].Should().HaveCount(1); context.Errors["entered.path.nested"].Should().ContainInOrder(661); } } } } ================================================ FILE: tests/Validot.Tests.Unit/ValidationTestData.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using Validot.Translations; public static class ValidationTestData { public class TestCase { public string Name { get; set; } public Specification Specification { get; set; } public IReadOnlyDictionary> ExpectedTemplate { get; set; } public IReadOnlyList ValidationCases { get; set; } } public class ValidationTestCase { public TestClass Model { get; set; } public IReadOnlyDictionary> Errors { get; set; } public string FailFastErrorKey { get; set; } public ReferenceLoopExceptionCase ReferenceLoopExceptionCase { get; set; } } public class ErrorTestCase { public IReadOnlyList Messages { get; set; } public IReadOnlyList Codes { get; set; } public IReadOnlyList Args { get; set; } } public class ReferenceLoopExceptionCase { public Type Type { get; set; } public string Path { get; set; } public string NestedPath { get; set; } } public class ArgTestCase { public string Name { get; set; } public dynamic Value { get; set; } } public class TestCollection : IEnumerable { private readonly IEnumerable _innerCollection; public TestCollection(IEnumerable innerCollection) { _innerCollection = innerCollection; } public IEnumerator GetEnumerator() => _innerCollection.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public class TestMember { public string MemberText { get; set; } public DateTimeOffset MemberDate { get; set; } public TestClass NestedSelf { get; set; } } public struct TestStruct { public int StructNumber { get; set; } } public class TestParent { public int ParentNumber { get; set; } } public class TestChild : TestParent { public int ChildNumber { get; set; } } public class TestClass { public string HybridField; public int ValueField; public string Hybrid { get; set; } public int Value { get; set; } public object Reference { get; set; } public bool? Nullable { get; set; } public TestClass Self { get; set; } public TestCollection SelfCollection { get; set; } public TestCollection Collection { get; set; } public TestMember Member { get; set; } public TestStruct StructMember { get; set; } public TestCollection MembersCollection { get; set; } public TestChild Child { get; set; } public Dictionary ComplexDictionary { get; set; } public Dictionary SimpleDictionary { get; set; } } private static readonly Dictionary> NoErrors = new Dictionary>(); public static TestCase RenamedClone(TestCase input, string prefix) => new TestCase() { Name = $"{prefix}_{input.Name}", Specification = input.Specification, ValidationCases = input.ValidationCases, ExpectedTemplate = input.ExpectedTemplate }; public static IReadOnlyList GetCases() { var cases = new List(); cases.AddRange(GlobalPresenceCases.Select(c => RenamedClone(c, nameof(GlobalPresenceCases)))); cases.AddRange(MemberPresenceCases.Select(c => RenamedClone(c, nameof(MemberPresenceCases)))); cases.AddRange(SingleRuleCases.Select(c => RenamedClone(c, nameof(SingleRuleCases)))); cases.AddRange(SingleRuleTemplateCases.Select(c => RenamedClone(c, nameof(SingleRuleTemplateCases)))); cases.AddRange(RulesCases.Select(c => RenamedClone(c, nameof(RulesCases)))); cases.AddRange(MemberCases.Select(c => RenamedClone(c, nameof(MemberCases)))); cases.AddRange(CommandCases.Select(c => RenamedClone(c, nameof(CommandCases)))); cases.AddRange(PathCases.Select(c => RenamedClone(c, nameof(PathCases)))); cases.AddRange(OverwritingCases.Select(c => RenamedClone(c, nameof(OverwritingCases)))); cases.AddRange(FailFastCases.Select(c => RenamedClone(c, nameof(FailFastCases)))); cases.AddRange(MixedCases.Select(c => RenamedClone(c, nameof(MixedCases)))); cases.AddRange(ReferencesLoopCases().Select(c => RenamedClone(c, nameof(ReferencesLoopCases)))); return cases; } public static IReadOnlyList GlobalPresenceCases { get; } = new[] { new TestCase() { Name = "optional", Specification = s => s.Optional(), ExpectedTemplate = NoErrors, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null, }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "no_commands", Specification = s => s, ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required", Specification = s => s.Required(), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_custom_message", Specification = s => s.Required().WithMessage("Custom Message"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_extra_custom_message", Specification = s => s.Required().WithExtraMessage("Extra Custom Message"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, "Extra Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, "Extra Custom Message" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_custom_code", Specification = s => s.Required().WithCode("Code1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_extra_custom_code", Specification = s => s.Required().WithExtraCode("Extra_Custom_Code"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, }, Codes = new[] { "Extra_Custom_Code" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, }, Codes = new[] { "Extra_Custom_Code" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_mix", Specification = s => s.Required().WithMessage("Message 1").WithExtraMessage("Message 2").WithExtraCode("Code1").WithExtraCode("Code2"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_mix_with_more_commands_after", Specification = s => s .Required().WithMessage("Message 1").WithExtraMessage("Message 2").WithExtraCode("Code1").WithExtraCode("Code2") .Rule(m => true).WithMessage("rule message 1") .Rule(m => true).WithCode("rule_code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } }, new ErrorTestCase() { Messages = new[] { "rule message 1" }, }, new ErrorTestCase() { Codes = new[] { "rule_code_1" }, }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "forbidden", Specification = s => s.Forbidden(), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "forbidden_custom_message", Specification = s => s.Forbidden().WithMessage("Custom Message"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "forbidden_extra_custom_message", Specification = s => s.Forbidden().WithExtraMessage("Extra Custom Message"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden, "Extra Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden, "Extra Custom Message" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "forbidden_custom_code", Specification = s => s.Forbidden().WithCode("Code1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "forbidden_extra_custom_code", Specification = s => s.Forbidden().WithExtraCode("Code1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden }, Codes = new[] { "Code1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden }, Codes = new[] { "Code1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "forbidden_mix", Specification = s => s.Forbidden().WithMessage("Message 1").WithExtraMessage("Message 2").WithExtraCode("Code1").WithExtraCode("Code2"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, FailFastErrorKey = "" }, }, }, }; public static IReadOnlyList MemberPresenceCases { get; } = new[] { new TestCase() { Name = "optional", Specification = s => s .Member(m => m.Member, m => m.Optional()), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null, }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "no_commands", Specification = s => s .Member(m => m.Member, m => m), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required", Specification = s => s .Member(m => m.Member, m => m.Required()), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_custom_message", Specification = s => s .Member(m => m.Member, m => m.Required().WithMessage("Custom Message")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_extra_custom_message", Specification = s => s .Member(m => m.Member, m => m.Required().WithExtraMessage("Extra Custom Message")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, "Extra Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, "Extra Custom Message" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_custom_code", Specification = s => s .Member(m => m.Member, m => m.Required().WithCode("Code1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_extra_custom_code", Specification = s => s .Member(m => m.Member, m => m.Required().WithExtraCode("Extra_Custom_Code")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, }, Codes = new[] { "Extra_Custom_Code" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, }, Codes = new[] { "Extra_Custom_Code" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_mix", Specification = s => s .Member(m => m.Member, m => m.Required().WithMessage("Message 1").WithExtraMessage("Message 2").WithExtraCode("Code1").WithExtraCode("Code2")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "required_mix_with_more_commands_after", Specification = s => s .Member(m => m.Member, m => m .Required().WithMessage("Message 1").WithExtraMessage("Message 2").WithExtraCode("Code1").WithExtraCode("Code2") .Rule(m1 => true).WithMessage("rule message 1") .Rule(m1 => true).WithCode("rule_code_1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } }, new ErrorTestCase() { Messages = new[] { "rule message 1" }, }, new ErrorTestCase() { Codes = new[] { "rule_code_1" }, }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "forbidden", Specification = s => s .Member(m => m.Member, m => m.Forbidden()), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden } } } }, FailFastErrorKey = "Member" }, }, }, new TestCase() { Name = "forbidden_custom_message", Specification = s => s .Member(m => m.Member, m => m.Forbidden().WithMessage("Custom Message")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Custom Message" } } } }, FailFastErrorKey = "Member" }, }, }, new TestCase() { Name = "forbidden_extra_custom_message", Specification = s => s .Member(m => m.Member, m => m.Forbidden().WithExtraMessage("Extra Custom Message")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden, "Extra Custom Message" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden, "Extra Custom Message" } } } }, FailFastErrorKey = "Member" }, }, }, new TestCase() { Name = "forbidden_custom_code", Specification = s => s .Member(m => m.Member, m => m.Forbidden().WithCode("Code1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Codes = new[] { "Code1" } } } }, FailFastErrorKey = "Member" }, }, }, new TestCase() { Name = "forbidden_extra_custom_code", Specification = s => s .Member(m => m.Member, m => m.Forbidden().WithExtraCode("Code1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden }, Codes = new[] { "Code1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Forbidden }, Codes = new[] { "Code1" } } } }, FailFastErrorKey = "Member" }, }, }, new TestCase() { Name = "forbidden_mix", Specification = s => s .Member(m => m.Member, m => m.Forbidden().WithMessage("Message 1").WithExtraMessage("Message 2").WithExtraCode("Code1").WithExtraCode("Code2")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "Message 1", "Message 2" }, Codes = new[] { "Code1", "Code2" } } } }, FailFastErrorKey = "Member" }, }, }, }; public static IReadOnlyList MemberCases { get; } = new[] { new TestCase() { Name = "reference", Specification = s => s .Member(m => m.Member, m => m.Rule(m1 => m1.MemberText != null).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, } }, new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Member = null }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, } } }, }, FailFastErrorKey = "Member", }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = null } }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "value", Specification = s => s .Member(m => m.StructMember, m => m.Rule(m1 => m1.StructNumber != 0).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { StructMember = new TestStruct() }, Errors = new Dictionary>() { ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "StructMember", }, new ValidationTestCase() { Model = new TestClass() { StructMember = new TestStruct() { StructNumber = 321 } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "collection", Specification = s => s .Member(m => m.Collection, m => m.Rule(m1 => m1.ElementAt(2) != 0).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, } }, new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Collection = null }, Errors = new Dictionary>() { ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, }, FailFastErrorKey = "Collection", }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 0, 0, 0 }) }, Errors = new Dictionary>() { ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "Collection", }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 1, 2, 3, 4 }) }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "nullable", Specification = s => s .Member(m => m.Nullable, m => m.Rule(m1 => m1.HasValue && m1.Value).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, } }, new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Nullable = null }, Errors = new Dictionary>() { ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, }, FailFastErrorKey = "Nullable", }, new ValidationTestCase() { Model = new TestClass() { Nullable = false }, Errors = new Dictionary>() { ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "Nullable", }, new ValidationTestCase() { Model = new TestClass() { Nullable = true }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "field", Specification = s => s .Member(m => m.HybridField, m => m.Rule(m1 => !string.IsNullOrEmpty(m1)).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["HybridField"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required, } }, new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { HybridField = null }, Errors = new Dictionary>() { ["HybridField"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, }, FailFastErrorKey = "HybridField", }, new ValidationTestCase() { Model = new TestClass() { HybridField = "" }, Errors = new Dictionary>() { ["HybridField"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "HybridField", }, new ValidationTestCase() { Model = new TestClass() { HybridField = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, }, }, }; public static IReadOnlyList SingleRuleCases { get; } = new[] { new TestCase() { Name = "rule", Specification = s => s.Rule(m => m.Value == 0), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { MessageKey.Global.Error } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Error } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_message", Specification = s => s.Rule(m => m.Value == 0).WithMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_extra_message", Specification = s => s.Rule(m => m.Value == 0).WithExtraMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_code", Specification = s => s.Rule(m => m.Value == 0).WithCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "code_1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_extra_code", Specification = s => s.Rule(m => m.Value == 0).WithExtraCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "code_1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_messages_and_codes", Specification = s => s.Rule(m => m.Value == 0).WithMessage("message 1").WithExtraMessage("message 2").WithExtraCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_different_path", Specification = s => s.Rule(m => m.Value == 0).WithPath("extra").WithMessage("message 1").WithExtraMessage("message 2").WithExtraCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["extra"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { ["extra"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, FailFastErrorKey = "extra" }, }, }, }; public static IReadOnlyList SingleRuleTemplateCases { get; } = new[] { new TestCase() { Name = "rule", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message key" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message key" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_args", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key", Arg.Number("arg1", 123), Arg.Text("arg2", "xyz")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message key" }, Args = new[] { new ArgTestCase() { Name = "arg1", Value = 123 }, new ArgTestCase() { Name = "arg2", Value = "xyz" }, } }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message key" }, Args = new[] { new ArgTestCase() { Name = "arg1", Value = 123 }, new ArgTestCase() { Name = "arg2", Value = "xyz" }, } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_message", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key").WithMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_args_and_message", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key", Arg.Number("arg1", 123), Arg.Text("arg2", "xyz")).WithMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" }, Args = new[] { new ArgTestCase() { Name = "arg1", Value = 123 }, new ArgTestCase() { Name = "arg2", Value = "xyz" }, } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" }, Args = new[] { new ArgTestCase() { Name = "arg1", Value = 123 }, new ArgTestCase() { Name = "arg2", Value = "xyz" }, } } }, }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_extra_message", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key").WithExtraMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message key", "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message key", "message 1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_code", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key").WithCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Codes = new[] { "code_1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_extra_code", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key").WithExtraCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message key" }, Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message key" }, Codes = new[] { "code_1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_messages_and_codes", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key").WithMessage("message 1").WithExtraMessage("message 2").WithExtraCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "rule_with_different_path", Specification = s => s.RuleTemplate(m => m.Value == 0, "message key").WithPath("extra").WithMessage("message 1").WithExtraMessage("message 2").WithExtraCode("code_1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["extra"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { ["extra"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1", "message 2" }, Codes = new[] { "code_1" } } } }, FailFastErrorKey = "extra" }, }, }, }; public static IReadOnlyList RulesCases { get; } = new[] { new TestCase() { Name = "no_rules", Specification = s => s, ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "single_rule", Specification = s => s.Rule(m => m.Value == 0).WithMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "single_rule_with_condition", Specification = s => s.Rule(m => m.Value == 0).WithCondition(m => m.Nullable == true).WithMessage("message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = false }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = true }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = false }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = true }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "many_rules", Specification = s => s .Rule(m => m.Value == 0).WithMessage("message 1") .Rule(m => m.Nullable == false).WithMessage("message 21").WithExtraMessage("message 22") .Rule(m => m.Reference == null).WithMessage("message 3").WithExtraCode("code_3"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = false, Reference = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = true, Reference = new object() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = true, Reference = null }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = false, Reference = new object() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "" }, }, }, new TestCase() { Name = "many_rules_with_custom_unique_paths", Specification = s => s .Rule(m => m.Value == 0).WithPath("Name1").WithMessage("message 1") .Rule(m => m.Nullable == false).WithPath("Name2").WithMessage("message 21").WithExtraMessage("message 22") .Rule(m => m.Reference == null).WithPath("Name31.Name32").WithMessage("message 3").WithExtraCode("code_3"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["Name1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Name2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, }, ["Name31.Name32"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = false, Reference = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = true, Reference = new object() }, Errors = new Dictionary>() { ["Name1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Name2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, }, ["Name31.Name32"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "Name1" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = true, Reference = null }, Errors = new Dictionary>() { ["Name2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, }, }, FailFastErrorKey = "Name2" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = false, Reference = new object() }, Errors = new Dictionary>() { ["Name1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Name31.Name32"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "Name1" }, }, }, new TestCase() { Name = "many_rules_with_custom_paths", Specification = s => s .Rule(m => m.Value == 0).WithPath("Name1").WithMessage("message 1") .Rule(m => m.Nullable == false).WithPath("Name2").WithMessage("message 21").WithExtraMessage("message 22") .Rule(m => m.Reference == null).WithPath("Name1").WithMessage("message 3").WithExtraCode("code_3"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["Name1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } }, ["Name2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = false, Reference = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = true, Reference = new object() }, Errors = new Dictionary>() { ["Name1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } }, ["Name2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, }, }, FailFastErrorKey = "Name1" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = true, Reference = null }, Errors = new Dictionary>() { ["Name2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, }, }, FailFastErrorKey = "Name2" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = false, Reference = new object() }, Errors = new Dictionary>() { ["Name1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "Name1" }, }, }, new TestCase() { Name = "many_rules_with_conditions", Specification = s => s .Rule(m => m.Value == 0).WithCondition(t => false).WithMessage("message 1") .Rule(m => m.Nullable == false).WithCondition(t => true).WithMessage("message 21").WithExtraMessage("message 22") .Rule(m => m.Reference == null).WithCondition(t => true).WithMessage("message 3").WithExtraCode("code_3"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = false, Reference = null }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = true, Reference = new object() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Nullable = true, Reference = null }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 21", "message 22" } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Nullable = false, Reference = new object() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" }, Codes = new[] { "code_3" } } } }, FailFastErrorKey = "" }, }, }, }; public static IReadOnlyList CommandCases { get; } = new[] { new TestCase() { Name = "Member", Specification = s => s.Member(m => m.Member, m => m.Rule(m1 => m1.MemberText != null).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsCollection", Specification = s => s.Member(m => m.MembersCollection, m => m .AsCollection, TestMember>(c => c.Rule(c1 => c1.MemberText != null).WithMessage("message 1")) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["MembersCollection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["MembersCollection.#"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["MembersCollection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "MembersCollection" }, new ValidationTestCase() { Model = new TestClass() { MembersCollection = new TestCollection(Array.Empty()) }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { MembersCollection = new TestCollection( new TestMember[] { new TestMember() { MemberText = "abc" }, null, new TestMember(), new TestMember() { MemberText = "abc" }, new TestMember(), null }) }, Errors = new Dictionary>() { ["MembersCollection.#1"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["MembersCollection.#2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["MembersCollection.#4"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["MembersCollection.#5"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, }, FailFastErrorKey = "MembersCollection.#1" }, new ValidationTestCase() { Model = new TestClass() { MembersCollection = new TestCollection( new TestMember[] { new TestMember() { MemberText = "abc" }, new TestMember() { MemberText = "abc" }, new TestMember(), null, }) }, Errors = new Dictionary>() { ["MembersCollection.#2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["MembersCollection.#3"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, }, FailFastErrorKey = "MembersCollection.#2" }, new ValidationTestCase() { Model = new TestClass() { MembersCollection = new TestCollection( new TestMember[] { new TestMember() { MemberText = "abc" }, new TestMember() { MemberText = "abc" }, }) }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsNullable", Specification = s => s.Member(m => m.Nullable, m => m .AsNullable(m1 => m1.Rule(m2 => m2).WithMessage("message 1")) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Nullable" }, new ValidationTestCase() { Model = new TestClass() { Nullable = false }, Errors = new Dictionary>() { ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Nullable" }, new ValidationTestCase() { Model = new TestClass() { Nullable = true }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsConverted", Specification = s => s.Member(m => m.Value, m => m .AsConverted(value => value.ToString(CultureInfo.InvariantCulture), v => v.Rule(str => str.Length <= 4).WithMessage("Number must be written using no more than 4 digits.")) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Value"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "Number must be written using no more than 4 digits." } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Value = 123456 }, Errors = new Dictionary>() { ["Value"] = new[] { new ErrorTestCase() { Messages = new[] { "Number must be written using no more than 4 digits." } }, } }, FailFastErrorKey = "Value" }, new ValidationTestCase() { Model = new TestClass() { Value = 12 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Nullable = true }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsType", Specification = s => s.Member(m => m.Child, m => m .AsType(new Specification(p => p .Member(p1 => p1.ParentNumber, n => n.NonZero().WithMessage("Must not be zero"))) ) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Child"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Child.ParentNumber"] = new[] { new ErrorTestCase() { Messages = new[] { "Must not be zero" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { Child = new TestChild() { ParentNumber = 0 } }, Errors = new Dictionary>() { ["Child.ParentNumber"] = new[] { new ErrorTestCase() { Messages = new[] { "Must not be zero" } } }, }, FailFastErrorKey = "Child.ParentNumber" }, new ValidationTestCase() { Model = new TestClass() { Child = new TestChild() { ParentNumber = 10 } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsModel", Specification = s => s.Member(m => m.Member, m => m .AsModel(m1 => m1.Rule(m2 => m2.MemberText != null).WithMessage("message 1"))), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsDictionary_Simple", Specification = s => s.Member(m => m.SimpleDictionary, m => m .AsDictionary(m1 => m1 .Rule(v => v.Length == 3).WithMessage("message 3") .Rule(v => v.Contains("x")).WithMessage("message x") ) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["SimpleDictionary"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["SimpleDictionary.#"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { SimpleDictionary = new Dictionary() { { "key1", "xx1" }, { "key2", "xxx2" }, { "key3", "xx2" }, { "key4", "xx4" }, { "key5", "yy5" }, { "key6", "xx6" }, { "key7", "oops" }, { "key8", "8xx" }, { "key9", null }, } }, Errors = new Dictionary>() { ["SimpleDictionary.key2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.key5"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.key7"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.key9"] = new[] { new ErrorTestCase() { Messages = new[] { "Global.Required" } }, }, }, FailFastErrorKey = "SimpleDictionary.key2" }, new ValidationTestCase() { Model = new TestClass() { SimpleDictionary = new Dictionary() { { "key.", "x with dot at end" }, { "key 2", "x with space" }, { "key .", "x with space and dot" }, { "k.e.y", "with dots" }, { "...ke....y..", "with dots" }, { "k e y", "with spaces" }, { " ", "x with three spaces" }, { string.Empty, "x with empty" }, { ">() { ["SimpleDictionary.key"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.key 2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.key "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.k.e.y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.ke.y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.k e y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary. "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary. "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.key3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.key4"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary. < () { { "<>() { ["SimpleDictionary.key"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.key2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.key3"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, }, FailFastErrorKey = "SimpleDictionary.key", }, new ValidationTestCase() { Model = new TestClass() { SimpleDictionary = new Dictionary() }, Errors = NoErrors, FailFastErrorKey = null, }, new ValidationTestCase() { Model = new TestClass() { SimpleDictionary = new Dictionary() { { "key1", "xx1" }, { "key2", "xx2" }, { "key3", "xx3" }, { "< s.Member(m => m.SimpleDictionary, m => m .AsDictionary( m1 => m1 .Rule(v => v.Length == 3).WithMessage("message 3") .Rule(v => v.Contains("x")).WithMessage("message x"), key => key.ToUpperInvariant() ) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["SimpleDictionary"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["SimpleDictionary.#"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { SimpleDictionary = new Dictionary() { { "key1", "xx1" }, { "key2", "xxx2" }, { "key3", "xx2" }, { "key4", "xx4" }, { "key5", "yy5" }, { "key6", "xx6" }, { "key7", "oops" }, { "key8", "8xx" }, { "key9", null }, } }, Errors = new Dictionary>() { ["SimpleDictionary.KEY2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.KEY5"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.KEY7"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.KEY9"] = new[] { new ErrorTestCase() { Messages = new[] { "Global.Required" } }, }, }, FailFastErrorKey = "SimpleDictionary.KEY2" }, new ValidationTestCase() { Model = new TestClass() { SimpleDictionary = new Dictionary() { { "key.", "x with dot at end" }, { "key 2", "x with space" }, { "key .", "x with space and dot" }, { "k.e.y", "with dots" }, { "...ke....y..", "with dots" }, { "k e y", "with spaces" }, { " ", "x with three spaces" }, { string.Empty, "x with empty" }, { ">() { ["SimpleDictionary.KEY"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.KEY 2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.KEY "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.K.E.Y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.KE.Y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.K E Y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary. "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary. "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.KEY3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.KEY4"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary. < () { { "<>() { ["SimpleDictionary.KEY"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["SimpleDictionary.KEY2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["SimpleDictionary.KEY3"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, }, FailFastErrorKey = "SimpleDictionary.KEY", }, } }, new TestCase() { Name = "AsDictionary_Complex", Specification = s => s.Member(m => m.ComplexDictionary, m => m.AsDictionary( m1 => m1 .Rule(v => v.Hybrid?.Length == 3).WithMessage("message 3") .Rule(v => v.Hybrid?.Contains("x") == true).WithMessage("message x"), k => k.MemberText.ToUpperInvariant()) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["ComplexDictionary"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["ComplexDictionary.#"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { ComplexDictionary = new Dictionary() { { new TestMember() { MemberText = "key1" }, new TestClass() { Hybrid = "xx1" } }, { new TestMember() { MemberText = "key2" }, new TestClass() { Hybrid = "xxx2" } }, { new TestMember() { MemberText = "key3" }, new TestClass() { Hybrid = "xx2" } }, { new TestMember() { MemberText = "key4" }, new TestClass() { Hybrid = "xx4" } }, { new TestMember() { MemberText = "key5" }, new TestClass() { Hybrid = "yy5" } }, { new TestMember() { MemberText = "key6" }, new TestClass() { Hybrid = "xx6" } }, { new TestMember() { MemberText = "key7" }, new TestClass() { Hybrid = "oops" } }, { new TestMember() { MemberText = "key8" }, new TestClass() { Hybrid = "8xx" } }, { new TestMember() { MemberText = "key9" }, new TestClass() { Hybrid = null } }, { new TestMember() { MemberText = "key10" }, null }, } }, Errors = new Dictionary>() { ["ComplexDictionary.KEY2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary.KEY5"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary.KEY7"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary.KEY9"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary.KEY10"] = new[] { new ErrorTestCase() { Messages = new[] { "Global.Required" } }, }, }, FailFastErrorKey = "ComplexDictionary.KEY2" }, new ValidationTestCase() { Model = new TestClass() { ComplexDictionary = new Dictionary() { { new TestMember() { MemberText = "key." }, new TestClass() { Hybrid = "x with dot at end" } }, { new TestMember() { MemberText = "key 2" }, new TestClass() { Hybrid = "x with space" } }, { new TestMember() { MemberText = "key ." }, new TestClass() { Hybrid = "x with space and dot" } }, { new TestMember() { MemberText = "k.e.y" }, new TestClass() { Hybrid = "with dots" } }, { new TestMember() { MemberText = "...ke....y.." }, new TestClass() { Hybrid = "with dots" } }, { new TestMember() { MemberText = "k e y" }, new TestClass() { Hybrid = "with spaces" } }, { new TestMember() { MemberText = " " }, new TestClass() { Hybrid = "x with three spaces" } }, { new TestMember() { MemberText = string.Empty }, new TestClass() { Hybrid = "x with empty" } }, { new TestMember() { MemberText = ">() { ["ComplexDictionary.KEY"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary.KEY 2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary.KEY "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary.K.E.Y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary.KE.Y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary.K E Y"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary. "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary. "] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary.KEY3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, }, ["ComplexDictionary.KEY4"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ComplexDictionary. < () }, Errors = NoErrors, FailFastErrorKey = null, }, new ValidationTestCase() { Model = new TestClass() { ComplexDictionary = new Dictionary() { { new TestMember() { MemberText = "key1" }, new TestClass() { Hybrid = "xx1" } }, { new TestMember() { MemberText = "key2" }, new TestClass() { Hybrid = "xx2" } }, { new TestMember() { MemberText = "< PathCases { get; } = new[] { new TestCase() { Name = "Member_Raw", Specification = s => s .Member(m => m.Member, m => m.Rule(m1 => m1.MemberText != null).WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Member_Rule_LevelUp", Specification = s => s .Member(m => m.Member, m => m.Rule(m1 => m1.MemberText != null).WithPath("<").WithMessage("message 1")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Member_Renamed", Specification = s => s .Member(m => m.Member, m => m.Rule(m1 => m1.MemberText != null).WithMessage("message 1")).WithPath("Renamed"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Renamed"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Renamed"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Renamed" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Renamed"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Renamed" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Member_Rule_LevelDown", Specification = s => s .Member(m => m.Member, m => m .Rule(m1 => m1.MemberText != null).WithPath("SomeInnerName").WithMessage("message 1") ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member.SomeInnerName"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member.SomeInnerName"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member.SomeInnerName" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Member_LevelDown", Specification = s => s .Member(m => m.Member, m => m .Rule(m1 => m1.MemberText != null).WithMessage("message 1") ).WithPath("Member.SomeInnerName"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member.SomeInnerName"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member.SomeInnerName"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Member.SomeInnerName" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member.SomeInnerName"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member.SomeInnerName" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "MultipleCommandsToSinglePath", Specification = s => s .Member(m => m.Member, m => m .Rule(m1 => m1.MemberText != null).WithMessage("message 1") ).WithPath("Single.Path") .Member(m => m.Collection, m => m .AsCollection, int>(m1 => m1.Rule(m3 => m3 != 0).WithPath("<").WithMessage("message 2")) ).WithPath("Single.Path") .Member(m => m.Collection, m => m .AsCollection, int>(m1 => m1.Rule(m3 => m3 != 0).WithPath("< m.Nullable, m => m .AsNullable(m1 => m1.Rule(m3 => m3).WithMessage("message 4")) ).WithPath("Single.Path") .Member(m => m.Nullable, m => m .AsNullable(m1 => m1.Rule(m3 => m3).WithPath(" m.Self, m => m .AsModel(m1 => m1.Rule(m3 => m3.Value != 0).WithMessage("message 6")) ).WithPath(" m.Self, m => m .AsModel(m1 => m1.Rule(m3 => m3.Value != 0).WithPath(" m1.Rule(m3 => m3.Value != 0).WithPath("Single.Path").WithMessage("message 8")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Self"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Single.Path"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } }, new ErrorTestCase() { Messages = new[] { "message 2" } }, new ErrorTestCase() { Messages = new[] { "message 3" } }, new ErrorTestCase() { Messages = new[] { "message 4" } }, new ErrorTestCase() { Messages = new[] { "message 5" } }, new ErrorTestCase() { Messages = new[] { "message 6" } }, new ErrorTestCase() { Messages = new[] { "message 7" } }, new ErrorTestCase() { Messages = new[] { "message 8" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Self"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Single.Path"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 8" } } }, }, FailFastErrorKey = "Single.Path" }, }, }, new TestCase() { Name = "CollectionLevels", Specification = s => s .Member(m => m.Collection, m => m .AsCollection, int>(m1 => m1 .Rule(m3 => m3 != 0).WithPath("Zero").WithMessage("message !0") .Rule(m3 => m3 != 1).WithPath("<").WithMessage("message !1") .Rule(m3 => m3 != 2).WithPath("< m3 != 3).WithPath("Down.Down").WithMessage("message !3") ) ), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Collection.#.Zero"] = new[] { new ErrorTestCase() { Messages = new[] { "message !0" } } }, ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message !1" } } }, ["Up"] = new[] { new ErrorTestCase() { Messages = new[] { "message !2" } } }, ["Collection.#.Down.Down"] = new[] { new ErrorTestCase() { Messages = new[] { "message !3" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 1, 2, 3, 0, 3 }) }, Errors = new Dictionary>() { ["Collection.#0.Zero"] = new[] { new ErrorTestCase() { Messages = new[] { "message !0" } } }, ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { "message !1" } } }, ["Up"] = new[] { new ErrorTestCase() { Messages = new[] { "message !2" } } }, ["Collection.#3.Down.Down"] = new[] { new ErrorTestCase() { Messages = new[] { "message !3" } } }, ["Collection.#4.Zero"] = new[] { new ErrorTestCase() { Messages = new[] { "message !0" } } }, ["Collection.#5.Down.Down"] = new[] { new ErrorTestCase() { Messages = new[] { "message !3" } } }, }, FailFastErrorKey = "Collection.#0.Zero" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 2, 2, 1, 1 }) }, Errors = new Dictionary>() { ["Up"] = new[] { new ErrorTestCase() { Messages = new[] { "message !2" } }, new ErrorTestCase() { Messages = new[] { "message !2" } } }, ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { "message !1" } }, new ErrorTestCase() { Messages = new[] { "message !1" } } }, }, FailFastErrorKey = "Up" }, }, }, new TestCase() { Name = "ChainedAsModel_RenamedInside", Specification = s => s .AsModel(m => m.Rule(m1 => m1.Collection.Contains(0)).WithPath("M0").WithMessage("message 0")) .AsModel(m => m.Rule(m1 => m1.Collection.Contains(1)).WithPath("M1").WithMessage("message 1")) .AsModel(m => m.Rule(m1 => m1.Collection.Contains(2)).WithPath("M2").WithMessage("message 2")) .AsModel(m => m.Rule(m1 => m1.Collection.Contains(3)).WithPath("M3").WithMessage("message 3")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["M0"] = new[] { new ErrorTestCase() { Messages = new[] { "message 0" } } }, ["M1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["M2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 2" } } }, ["M3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 2, }) }, Errors = new Dictionary>() { ["M1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["M3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } } }, }, FailFastErrorKey = "M1" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 2, }) }, Errors = new Dictionary>() { ["M1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["M3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } } }, }, FailFastErrorKey = "M1" }, }, }, new TestCase() { Name = "ChainedAsModel_RenamedOutside", Specification = s => s .AsModel(m => m.Rule(m1 => m1.Collection.Contains(0)).WithMessage("message 0")).WithPath("M0") .AsModel(m => m.Rule(m1 => m1.Collection.Contains(1)).WithMessage("message 1")).WithPath("M1") .AsModel(m => m.Rule(m1 => m1.Collection.Contains(2)).WithMessage("message 2")).WithPath("M2") .AsModel(m => m.Rule(m1 => m1.Collection.Contains(3)).WithMessage("message 3")).WithPath("M3"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["M0"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 0" } } }, ["M1"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["M2"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 2" } } }, ["M3"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 3" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 2, }) }, Errors = new Dictionary>() { ["M1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["M3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } } }, }, FailFastErrorKey = "M1" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 2, }) }, Errors = new Dictionary>() { ["M1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } } }, ["M3"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } } }, }, FailFastErrorKey = "M1" }, }, }, }; public static IReadOnlyList OverwritingCases { get; } = new[] { new TestCase() { Name = "Member_SingleInnerRule", Specification = s => s .Member(m => m.Member, m => m.Rule(m1 => m1.MemberText != null).WithMessage("inner message 1")).WithMessage("outer message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Member_MultipleInnerRules", Specification = s => s .Member(m => m.Member, m => m .Rule(m1 => m1.MemberText != null).WithMessage("inner message 1") .Rule(m1 => m1.MemberText?.Contains("x") == true).WithMessage("inner message 2") .Rule(m1 => m1.MemberText?.Contains("z") == true).WithMessage("inner message 3") ).WithMessage("outer message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "abc" } }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xxx" } }, Errors = new Dictionary>() { ["Member"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "Member" }, new ValidationTestCase() { Model = new TestClass() { Member = new TestMember() { MemberText = "xyz" } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsModel_SingleInnerRule", Specification = s => s .AsModel(m => m.Rule(m1 => m1.Hybrid != null).WithMessage("inner message 1")).WithMessage("outer message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "outer message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "AsModel_MultipleInnerRules", Specification = s => s .AsModel(m => m .Rule(m1 => m1.Hybrid != null).WithMessage("inner message 1") .Rule(m1 => m1.Hybrid?.Contains("x") == true).WithMessage("inner message 2") .Rule(m1 => m1.Hybrid?.Contains("z") == true).WithMessage("inner message 3") ).WithMessage("outer message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "outer message 1" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "" }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "abc" }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xxx" }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Collection", Specification = s => s .Member(m => m.SelfCollection, m => m.AsCollection, TestClass>(mx => mx .Rule(m1 => m1.Hybrid != null).WithMessage("inner message 1") .Rule(m1 => m1.Hybrid?.Contains("x") == true).WithMessage("inner message 2") .Rule(m1 => m1.Hybrid?.Contains("z") == true).WithMessage("inner message 3") )).WithMessage("outer message 1"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } } }, ["SelfCollection"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["SelfCollection"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "SelfCollection" }, new ValidationTestCase() { Model = new TestClass() { SelfCollection = new TestCollection(new TestClass[] { }) }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { SelfCollection = new TestCollection(new TestClass[] { new TestClass() { Hybrid = "abc" }, new TestClass() { Hybrid = "xyz" }, new TestClass() { Hybrid = "xyz" }, }) }, Errors = new Dictionary>() { ["SelfCollection"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "SelfCollection" }, new ValidationTestCase() { Model = new TestClass() { SelfCollection = new TestCollection(new TestClass[] { new TestClass() { Hybrid = "xxx" }, new TestClass() { Hybrid = "xxx" }, new TestClass() { Hybrid = "xxx" }, }) }, Errors = new Dictionary>() { ["SelfCollection"] = new[] { new ErrorTestCase() { Messages = new[] { "outer message 1" } } } }, FailFastErrorKey = "SelfCollection" }, new ValidationTestCase() { Model = new TestClass() { SelfCollection = new TestCollection(new TestClass[] { new TestClass() { Hybrid = "xyz" }, new TestClass() { Hybrid = "xyz" }, new TestClass() { Hybrid = "xyz" }, }) }, Errors = NoErrors, FailFastErrorKey = null }, }, }, }; public static IReadOnlyList FailFastCases { get; } = new[] { new TestCase() { Name = "TopLevelRules", Specification = s => s .Rule(m => m.Hybrid?.Contains("x") == true).WithMessage("message x") .Rule(m => m.Hybrid?.Contains("y") == true).WithMessage("message y") .Rule(m => m.Hybrid?.Contains("z") == true).WithMessage("message z"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message y" } }, new ErrorTestCase() { Messages = new[] { "message z" } } }, }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, new ErrorTestCase() { Messages = new[] { "message y" } }, new ErrorTestCase() { Messages = new[] { "message z" } } } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Members", Specification = s => s .Member(m => m.Nullable, m => m.Rule(m1 => m1 == true).WithMessage("message 1")) .Member(m => m.Value, m => m.Rule(m1 => m1 == 1).WithMessage("message 2")) .Member(m => m.StructMember, m => m.Rule(m1 => m1.StructNumber == 1).WithMessage("message 3")), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Value"] = new[] { new ErrorTestCase() { Messages = new[] { "message 2" } }, }, ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Nullable = null, Value = 0, StructMember = new TestStruct() { StructNumber = 0 } }, Errors = new Dictionary>() { ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["Value"] = new[] { new ErrorTestCase() { Messages = new[] { "message 2" } }, }, ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, } }, FailFastErrorKey = "Nullable" }, new ValidationTestCase() { Model = new TestClass() { Nullable = false, Value = 1, StructMember = new TestStruct() { StructNumber = 0 } }, Errors = new Dictionary>() { ["Nullable"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, } }, FailFastErrorKey = "Nullable" }, new ValidationTestCase() { Model = new TestClass() { Nullable = true, Value = 0, StructMember = new TestStruct() { StructNumber = 0 } }, Errors = new Dictionary>() { ["Value"] = new[] { new ErrorTestCase() { Messages = new[] { "message 2" } }, }, ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, } }, FailFastErrorKey = "Value" }, new ValidationTestCase() { Model = new TestClass() { Nullable = true, Value = 1, StructMember = new TestStruct() { StructNumber = 0 } }, Errors = new Dictionary>() { ["StructMember"] = new[] { new ErrorTestCase() { Messages = new[] { "message 3" } }, } }, FailFastErrorKey = "StructMember" }, new ValidationTestCase() { Model = new TestClass() { Nullable = true, Value = 1, StructMember = new TestStruct() { StructNumber = 1 } }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "Collection", Specification = s => s .Member(m => m.Collection, m => m.AsCollection, int>(m1 => m1 .Rule(m2 => m2 != 0).WithMessage("message 1") )), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["Collection.#"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass(), Errors = new Dictionary>() { ["Collection"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, } }, FailFastErrorKey = "Collection" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 0, 1, 0 }) }, Errors = new Dictionary>() { ["Collection.#0"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Collection.#2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Collection.#0" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 1, 0, 0 }) }, Errors = new Dictionary>() { ["Collection.#1"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Collection.#2"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Collection.#1" }, new ValidationTestCase() { Model = new TestClass() { Collection = new TestCollection(new[] { 1, 1, 1 }) }, Errors = NoErrors, FailFastErrorKey = null }, }, }, }; public static IReadOnlyList MixedCases { get; } = new[] { new TestCase() { Name = "WithCondition_and_WithPath", Specification = s => s .Rule(m => m.Hybrid?.Contains("x") == true).WithCondition(m => m.HybridField?.Contains("x") == true).WithPath("XXX").WithMessage("message x") .Rule(m => m.Hybrid?.Contains("y") == true).WithCondition(m => m.HybridField?.Contains("y") == true).WithPath("YYY").WithMessage("message y") .Rule(m => m.Hybrid?.Contains("z") == true).WithCondition(m => m.HybridField?.Contains("z") == true).WithPath("ZZZ").WithMessage("message z"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["XXX"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["YYY"] = new[] { new ErrorTestCase() { Messages = new[] { "message y" } }, }, ["ZZZ"] = new[] { new ErrorTestCase() { Messages = new[] { "message z" } }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { HybridField = "xyz" }, Errors = new Dictionary>() { ["XXX"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["YYY"] = new[] { new ErrorTestCase() { Messages = new[] { "message y" } }, }, ["ZZZ"] = new[] { new ErrorTestCase() { Messages = new[] { "message z" } }, } }, FailFastErrorKey = "XXX" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "y", HybridField = "xyz" }, Errors = new Dictionary>() { ["XXX"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ZZZ"] = new[] { new ErrorTestCase() { Messages = new[] { "message z" } }, } }, FailFastErrorKey = "XXX" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xz", HybridField = "y" }, Errors = new Dictionary>() { ["YYY"] = new[] { new ErrorTestCase() { Messages = new[] { "message y" } }, }, }, FailFastErrorKey = "YYY" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz", HybridField = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, }, }, new TestCase() { Name = "WithCondition_and_WithPath_AndVersion", Specification = s => s .Rule(m => m.Hybrid?.Contains("x") == true).WithCondition(m => m.HybridField?.Contains("x") == true).WithPath("XXX").WithMessage("message x") .And() .Rule(m => m.Hybrid?.Contains("y") == true).WithCondition(m => m.HybridField?.Contains("y") == true).WithPath("YYY").WithMessage("message y") .And() .Rule(m => m.Hybrid?.Contains("z") == true).WithCondition(m => m.HybridField?.Contains("z") == true).WithPath("ZZZ").WithMessage("message z"), ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.Required } }, }, ["XXX"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["YYY"] = new[] { new ErrorTestCase() { Messages = new[] { "message y" } }, }, ["ZZZ"] = new[] { new ErrorTestCase() { Messages = new[] { "message z" } }, } }, ValidationCases = new[] { new ValidationTestCase() { Model = new TestClass() { HybridField = "xyz" }, Errors = new Dictionary>() { ["XXX"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["YYY"] = new[] { new ErrorTestCase() { Messages = new[] { "message y" } }, }, ["ZZZ"] = new[] { new ErrorTestCase() { Messages = new[] { "message z" } }, } }, FailFastErrorKey = "XXX" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "y", HybridField = "xyz" }, Errors = new Dictionary>() { ["XXX"] = new[] { new ErrorTestCase() { Messages = new[] { "message x" } }, }, ["ZZZ"] = new[] { new ErrorTestCase() { Messages = new[] { "message z" } }, } }, FailFastErrorKey = "XXX" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xz", HybridField = "y" }, Errors = new Dictionary>() { ["YYY"] = new[] { new ErrorTestCase() { Messages = new[] { "message y" } }, }, }, FailFastErrorKey = "YYY" }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz", HybridField = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Hybrid = "xyz" }, Errors = NoErrors, FailFastErrorKey = null }, }, }, }; public static IEnumerable ReferencesLoopCases() { Specification directSpecification = null; directSpecification = s => s .Optional() .Member(m => m.Self, directSpecification) .Rule(m => m.Value != 0).WithMessage("message 1"); TestClass direct1 = new TestClass(); direct1.Self = direct1; TestClass direct2 = new TestClass() { Self = new TestClass() { Self = new TestClass() { Self = new TestClass() } } }; direct2.Self.Self.Self.Self = direct2; TestClass direct3 = new TestClass() { Self = new TestClass() { Self = new TestClass() { Self = new TestClass() } } }; direct3.Self.Self.Self.Self = direct3.Self.Self; yield return new TestCase() { Name = "Direct", Specification = directSpecification, ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" }, }, }, ["Self"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.ReferenceLoop }, Args = new[] { new ArgTestCase() { Name = "type", Value = typeof(TestClass) } } }, }, }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Self = new TestClass() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Self"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Self" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Self = new TestClass() }, Errors = new Dictionary>() { ["Self"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Self" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Self = new TestClass() { Value = 0, Self = new TestClass() { Value = 0 } } }, Errors = new Dictionary>() { ["Self"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Self.Self"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Self.Self" }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Self = new TestClass() { Value = 2, Self = new TestClass() { Value = 3 } } }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = direct1, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestClass), NestedPath = "Self", Path = "" }, }, new ValidationTestCase() { Model = direct2, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestClass), NestedPath = "Self.Self.Self.Self", Path = "" }, }, new ValidationTestCase() { Model = direct3, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestClass), NestedPath = "Self.Self.Self.Self", Path = "Self.Self" }, }, }, }; Specification indirectSpecification1 = null; Specification indirectSpecification2; indirectSpecification2 = s => s .Optional() .Member(m => m.NestedSelf, indirectSpecification1); indirectSpecification1 = s => s .Optional() .Member(m => m.Member, indirectSpecification2) .Rule(m => m.Value != 0).WithMessage("message 1"); var indirect1 = new TestClass(); indirect1.Member = new TestMember() { NestedSelf = indirect1 }; var indirect2 = new TestClass(); indirect2.Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() } } } } } }; indirect2.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf = indirect2; var indirect3 = new TestClass(); indirect3.Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() } } } } } }; indirect3.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf = indirect3.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf; var indirect4 = new TestClass(); indirect4.Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() } } } } }; indirect4.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member = indirect4.Member; var indirect5 = new TestClass(); indirect5.Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() } } } } }; indirect5.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member = indirect5.Member.NestedSelf.Member.NestedSelf.Member; yield return new TestCase() { Name = "Indirect", Specification = indirectSpecification1, ExpectedTemplate = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" }, }, }, ["Member.NestedSelf"] = new[] { new ErrorTestCase() { Messages = new[] { MessageKey.Global.ReferenceLoop }, Args = new[] { new ArgTestCase() { Name = "type", Value = typeof(TestClass) } } }, }, }, ValidationCases = new[] { new ValidationTestCase() { Model = null, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 0 }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Member = new TestMember() }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, }, FailFastErrorKey = "" }, new ValidationTestCase() { Model = new TestClass() { Value = 0, Member = new TestMember() { NestedSelf = new TestClass() } }, Errors = new Dictionary>() { [""] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Member.NestedSelf"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member.NestedSelf" }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Member = new TestMember() { NestedSelf = new TestClass() { Member = new TestMember() { NestedSelf = new TestClass() } } } }, Errors = new Dictionary>() { ["Member.NestedSelf"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, }, ["Member.NestedSelf.Member.NestedSelf"] = new[] { new ErrorTestCase() { Messages = new[] { "message 1" } }, } }, FailFastErrorKey = "Member.NestedSelf.Member.NestedSelf" }, new ValidationTestCase() { Model = new TestClass() { Value = 1 }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = new TestClass() { Value = 1, Member = new TestMember() { NestedSelf = new TestClass() { Value = 1, Member = new TestMember() { NestedSelf = new TestClass() { Value = 1 } } } } }, Errors = NoErrors, FailFastErrorKey = null }, new ValidationTestCase() { Model = indirect1, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestClass), NestedPath = "Member.NestedSelf", Path = "" }, }, new ValidationTestCase() { Model = indirect2, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestClass), NestedPath = "Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf", Path = "" }, }, new ValidationTestCase() { Model = indirect3, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestClass), NestedPath = "Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member.NestedSelf", Path = "Member.NestedSelf.Member.NestedSelf.Member.NestedSelf" }, }, new ValidationTestCase() { Model = indirect4, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestMember), NestedPath = "Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member", Path = "Member" }, }, new ValidationTestCase() { Model = indirect5, ReferenceLoopExceptionCase = new ReferenceLoopExceptionCase() { Type = typeof(TestMember), NestedPath = "Member.NestedSelf.Member.NestedSelf.Member.NestedSelf.Member", Path = "Member.NestedSelf.Member.NestedSelf.Member" }, }, }, }; } public static IEnumerable CasesForTemplate_Data() { return GetCases().Select(c => new object[] { $"M_{c.Name}", c.Specification, c.ExpectedTemplate }); } public static IEnumerable CasesForValidation_Data() { foreach (var c in GetCases()) { var i = 0; foreach (var v in c.ValidationCases) { yield return new object[] { $"V_{c.Name}_{++i}", c.Specification, v.Model, v.Errors, v.ReferenceLoopExceptionCase }; } } } public static IEnumerable CasesForReferenceLoop_Data() { var cases = new List(); cases.AddRange(RulesCases.Select(c => RenamedClone(c, nameof(RulesCases)))); cases.AddRange(ReferencesLoopCases().Select(c => RenamedClone(c, nameof(ReferencesLoopCases)))); foreach (var c in cases) { var i = 0; foreach (var v in c.ValidationCases) { yield return new object[] { $"RL_T_{c.Name}_{++i}", true, c.Specification, v.Model, v.Errors, v.ReferenceLoopExceptionCase }; if (v.ReferenceLoopExceptionCase is null) { yield return new object[] { $"RL_F_{c.Name}_{++i}", false, c.Specification, v.Model, v.Errors, v.ReferenceLoopExceptionCase }; } } } } public static IEnumerable CasesForValidationWithFailFast_Data() { foreach (var c in GetCases()) { var i = 0; foreach (var v in c.ValidationCases) { yield return new object[] { $"F_{c.Name}_{++i}", c.Specification, v.Model, v.FailFastErrorKey is null ? NoErrors : new Dictionary>() { [v.FailFastErrorKey] = new[] { v.Errors[v.FailFastErrorKey][0] } }, v.ReferenceLoopExceptionCase }; } } } public static IEnumerable CasesForIsValid_Data() { foreach (var c in GetCases()) { var i = 0; foreach (var v in c.ValidationCases) { yield return new object[] { $"I_{c.Name}_{++i}", c.Specification, v.Model, v.Errors == null || !v.Errors.Any(), v.ReferenceLoopExceptionCase }; } } } public static IEnumerable CasesForFeed_Data() { foreach (var c in GetCases()) { var i = 0; foreach (var v in c.ValidationCases.Where(v => v.ReferenceLoopExceptionCase is null)) { yield return new object[] { $"FEED_{c.Name}_{++i}", c.Specification, v.Model, c.ExpectedTemplate, v.Errors, }; } } } public static IEnumerable CasesForFeedMultipleTimes_Data() { foreach (var c in GetCases()) { var caseData = c.ValidationCases .Where(v => v.ReferenceLoopExceptionCase is null) .Select(s => new { s.Model, s.Errors }).ToArray(); yield return new object[] { $"FEED_MULTIPLE_{c.Name}", c.Specification, caseData.Select(c1 => c1.Model).ToArray(), c.ExpectedTemplate, caseData.Select(c1 => c1.Errors).ToArray(), }; } } } } ================================================ FILE: tests/Validot.Tests.Unit/ValidationTestHelpers.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using FluentAssertions; using Validot.Errors; using Validot.Results; using Validot.Validation; using Validot.Validation.Stacks; public static class ValidationTestHelpers { public static void ShouldHaveTemplate(this IValidator validator, IReadOnlyDictionary> outputExpectations) { validator.Template.Should().NotBeNull(); var errorOutput = ((ValidationResult)validator.Template).GetErrorOutput(); errorOutput.ShouldMatchExpectations(outputExpectations); } public static void ShouldValidateAndHaveResult(this IValidator validator, T model, bool failFast, IReadOnlyDictionary> rawErrorsExpectations, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { IValidationResult result = null; Action action = () => { result = validator.Validate(model, failFast); }; if (exceptionCase is null) { action.Should().NotThrow(); var errorOutput = ((ValidationResult)result).GetErrorOutput(); errorOutput.ShouldMatchExpectations(rawErrorsExpectations); } else { var exceptionThrown = action.Should().ThrowExactly().Which; exceptionThrown.Type.Should().Be(exceptionCase.Type); exceptionThrown.Path.Should().Be(exceptionCase.Path); exceptionThrown.NestedPath.Should().Be(exceptionCase.NestedPath); } } public static void ShouldHaveIsValidTrueIfNoErrors(this IValidator validator, T model, bool expectedIsValid, ValidationTestData.ReferenceLoopExceptionCase exceptionCase) { bool isValid = false; Action action = () => { isValid = validator.IsValid(model); }; if (exceptionCase is null) { action.Should().NotThrow(); isValid.Should().Be(expectedIsValid); } else { var exceptionThrown = action.Should().ThrowExactly().Which; exceptionThrown.Type.Should().Be(exceptionCase.Type); exceptionThrown.Path.Should().BeNull(); exceptionThrown.NestedPath.Should().BeNull(); } } public static void ShouldMatchExpectations(this IReadOnlyDictionary> output, IReadOnlyDictionary> test) { output.Should().NotBeNull(); test.Should().NotBeNull(); output.Should().HaveCount(test.Count); foreach (var testPair in test) { output.Keys.Should().Contain(testPair.Key); output[testPair.Key].Should().NotBeNull(); output[testPair.Key].Should().HaveCount(testPair.Value.Count); for (var i = 0; i < testPair.Value.Count; ++i) { var testMessages = testPair.Value[i].Messages; var outputMessages = output[testPair.Key][i].Messages; if (testMessages is null) { outputMessages.Should().BeNullOrEmpty(); } else { outputMessages.Should().HaveCount(testMessages.Count); for (var j = 0; j < testMessages.Count; ++j) { outputMessages[j].Should().Be(testMessages[j]); } } var testCodes = testPair.Value[i].Codes; var outputCodes = output[testPair.Key][i].Codes; if (testCodes is null) { outputCodes.Should().BeNullOrEmpty(); } else { outputCodes.Should().HaveCount(testCodes.Count); for (var j = 0; j < testCodes.Count; ++j) { outputCodes[j].Should().Be(testCodes[j]); } } var testArgs = testPair.Value[i].Args; var outputArgs = output[testPair.Key][i].Args; if (testArgs is null) { outputArgs.Should().BeNullOrEmpty(); } else { outputArgs.Should().HaveCount(testArgs.Count); for (var j = 0; j < testArgs.Count; ++j) { outputArgs[j].Name.Should().Be(testArgs[j].Name); dynamic arg = outputArgs[j]; ((object)arg.Value.GetType()).Should().Be(((object)testArgs[j].Value).GetType()); ((object)arg.Value).Should().Be((object)testArgs[j].Value); } } } } } public static void ShouldMatchAmounts(this IErrorsHolder errorsHolder, IReadOnlyDictionary> test) { errorsHolder.Should().NotBeNull(); errorsHolder.Errors.Should().NotBeNull(); test.Should().NotBeNull(); errorsHolder.Errors.Should().HaveCount(test.Count); foreach (var testPair in test) { errorsHolder.Errors.Keys.Should().Contain(testPair.Key); errorsHolder.Errors[testPair.Key].Should().NotBeNull(); errorsHolder.Errors[testPair.Key].Should().HaveCount(testPair.Value.Count); } } } } ================================================ FILE: tests/Validot.Tests.Unit/ValidatorTests.cs ================================================ namespace Validot.Tests.Unit { using System; using System.Collections.Generic; using FluentAssertions; using NSubstitute; using Validot.Errors; using Validot.Errors.Args; using Validot.Settings; using Validot.Validation.Scheme; using Xunit; public class ValidatorTests { [Fact] public void Should_ThrowException_When_NullModelScheme() { Action action = () => _ = new Validator(null, Substitute.For()); action.Should().ThrowExactly().And.ParamName.Should().Be("modelScheme"); } [Fact] public void Should_ThrowException_When_NullSettings() { Action action = () => _ = new Validator(Substitute.For>(), null); action.Should().ThrowExactly().And.ParamName.Should().Be("settings"); } [Fact] public void Should_SetTemplate() { var modelScheme = Substitute.For>(); modelScheme.ErrorRegistry.Returns(new Dictionary() { [0] = new Error() { Messages = new[] { "Zero" }, Codes = new[] { "000" }, Args = Array.Empty() }, [1] = new Error() { Messages = new[] { "One" }, Codes = new[] { "111" }, Args = Array.Empty() } }); modelScheme.Template.Returns(new Dictionary>() { [""] = new[] { 0 }, ["path"] = new[] { 1 }, ["path.nested"] = new[] { 0, 1 } }); var settings = Substitute.For(); settings.Translations.Returns(new Dictionary>() { ["English"] = new Dictionary() { ["Zero"] = "English translated ZERO", ["One"] = "English translated ONE!!!" } }); var validator = new Validator(modelScheme, settings); validator.Template.Should().NotBeNull(); validator.Template.MessageMap[""].Should().HaveCount(1); validator.Template.MessageMap[""].Should().Contain("English translated ZERO"); validator.Template.MessageMap["path"].Should().HaveCount(1); validator.Template.MessageMap["path"].Should().Contain("English translated ONE!!!"); validator.Template.MessageMap["path.nested"].Should().HaveCount(2); validator.Template.MessageMap["path.nested"].Should().Contain("English translated ZERO", "English translated ONE!!!"); validator.Template.CodeMap[""].Should().HaveCount(1); validator.Template.CodeMap[""].Should().Contain("000"); validator.Template.CodeMap["path"].Should().HaveCount(1); validator.Template.CodeMap["path"].Should().Contain("111"); validator.Template.CodeMap["path.nested"].Should().HaveCount(2); validator.Template.CodeMap["path.nested"].Should().Contain("000", "111"); } [Fact] public void Should_SetSettings() { var settings = Substitute.For(); settings.Translations.Returns(new Dictionary>() { ["English"] = new Dictionary() { ["X"] = "XXX" } }); var modelScheme = Substitute.For>(); modelScheme.ErrorRegistry.Returns(new Dictionary() { [0] = new Error() { Messages = new[] { "X" }, Codes = Array.Empty(), Args = Array.Empty() } }); var validator = new Validator(modelScheme, settings); validator.Settings.Should().NotBeNull(); validator.Settings.Should().BeSameAs(settings); } [Fact] public void Factory_Should_NotBeNull() { Validator.Factory.Should().NotBeNull(); } } } ================================================ FILE: tests/Validot.Tests.Unit/Validot.Tests.Unit.csproj ================================================ net8.0 false runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: tests/Validot.Tests.Unit/ValidotExceptionTests.cs ================================================ namespace Validot.Tests.Unit { using System; using FluentAssertions; using Xunit; public class ValidotExceptionTests { [Fact] public void Should_Initialize_With_Message() { var exception = new ValidotException("a message"); exception.Message.Should().Be("a message"); } [Fact] public void Should_Initialize_With_Message_And_InnerException() { var innerException = new InvalidOperationException(); var exception = new ValidotException("a message", innerException); exception.Message.Should().Be("a message"); exception.InnerException.Should().BeSameAs(innerException); } } }