Skip to content

Commit

Permalink
Merged final v2.0.0 update changes
Browse files Browse the repository at this point in the history
  • Loading branch information
bartoszlenar committed Feb 1, 2021
2 parents 36d615f + a7d7f47 commit e0a0fb0
Show file tree
Hide file tree
Showing 24 changed files with 741 additions and 566 deletions.
147 changes: 102 additions & 45 deletions .github/README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion LICENSE
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Bartosz Lenar
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
Expand Down
22 changes: 20 additions & 2 deletions docs/CHANGELOG.md
Expand Up @@ -4,10 +4,28 @@ All notable changes to the [Validot project](https://github.com/bartoszlenar/Val
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).

## [Unreleased]
### Changed
## [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)
Expand Down
67 changes: 57 additions & 10 deletions docs/DOCUMENTATION.md
Expand Up @@ -2975,7 +2975,6 @@ validator.Settings.ReferenceLoopProtection; // false
- 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
Expand Down Expand Up @@ -3015,7 +3014,8 @@ object.ReferenceEquals(validator1.Settings, validator2.Settings) // true
#### Fetching holders

- Factory has `FetchHolders` method that scans the provided assemblies for [specification holders](#specification-holder).
- If no assembly is provided as a method's argument (function is called in a form of `Validator.Factory.FetchHolders()`), the factory will attempt to get them with the code: `AppDomain.CurrentDomain.GetAssemblies()`.
- 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<T>` interface.
- contains a parameterless constructor.
Expand Down Expand Up @@ -3071,15 +3071,57 @@ _Above, we can observe that the created validator respects the rules and the set
- 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 a big, single one - it doesn't matter.
- Call `Validator.Factory.FetchHolders()` to get the information about the holders and group the results by the `SpecifiedType`.
- 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.

_Below the code snippet for ASP.NET Core and its default `Microsoft.Extensions.DependencyInjection`:_

_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;
Expand All @@ -3089,7 +3131,11 @@ static class AddValidatorsExtensions
{
public static IServiceCollection AddValidators(this IServiceCollection @this, params Assembly[] assemblies)
{
var holders = Validator.Factory.FetchHolders(assemblies)
var assembliesToScan = assemblies.Length > 0
? assemblies
: AppDomain.CurrentDomain.GetAssemblies();

var holders = Validator.Factory.FetchHolders(assembliesToScan)
.GroupBy(h => h.SpecifiedType)
.Select(s => new
{
Expand All @@ -3107,21 +3153,22 @@ static class AddValidatorsExtensions
}
```

_And the example of usage within the app's `Startup.cs`:_

_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] will perform:
- 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`).
Expand Down Expand Up @@ -3639,7 +3686,7 @@ var result = validator.Validate(model);
result.TranslationNames; // [ "English" ]
```

- The list is exactly the same as in the [Validator](#validator) that produced the result.
- The list is the same as in the [Validator](#validator) that produced the result.

``` csharp
var validator = Validator.Factory.Create(specification, settings => settings
Expand Down
1 change: 0 additions & 1 deletion nuget.config
Expand Up @@ -2,6 +2,5 @@
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="RoslynMyGet" value="https://dotnet.myget.org/F/roslyn/api/v3/index.json" />
</packageSources>
</configuration>
195 changes: 95 additions & 100 deletions src/Validot/Errors/Args/NameArg.cs
@@ -1,100 +1,95 @@
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<Regex> _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<string> AllowedParameters => KeyAsAllowedParameters;

public string ToString(IReadOnlyDictionary<string, string> 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 (string.IsNullOrWhiteSpace(input))
{
return 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);
}
}
}
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<Regex> _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<string> AllowedParameters => KeyAsAllowedParameters;

public string ToString(IReadOnlyDictionary<string, string> 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);
}
}
}
2 changes: 1 addition & 1 deletion src/Validot/Factory/HolderInfo.cs
Expand Up @@ -26,7 +26,7 @@ internal HolderInfo(Type holderType, Type specifiedType)

if (!hasParameterlessConstructor)
{
throw new ArgumentException($"{holderType.GetFriendlyName()} must have parameterless constructor.", nameof(holderType));
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)))
Expand Down

0 comments on commit e0a0fb0

Please sign in to comment.