Giter Site home page Giter Site logo

bartoszlenar / validot Goto Github PK

View Code? Open in Web Editor NEW
295.0 12.0 18.0 1.27 MB

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.

License: MIT License

C# 99.70% Batchfile 0.01% PowerShell 0.14% Shell 0.12% Dockerfile 0.03%
validation validation-library validator specification fluent-api model-validation

validot's Introduction



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.


馃敟鈿旓笍 Validot vs FluentValidation 鈿旓笍馃敟

馃摐 Blogged: Validot's performance explained
馃摐 Blogged: Crafting model specifications using Validot

Built with 馃馃徎by Bartosz Lenar


Announcement! Version 3.0 is coming!

Brief update on the current state of the project:

  • Version 2.5 is the last version that will be released before the 3.0.
  • The upcoming version 3.0 will bring a few breaking changes, but the migration should be smooth and straightforward. And of course, it will be fully documented.
  • Version 2.5 is the last one targeting .NET Standard 2.0. The 3.0 will focus on the newest versions of .NET.

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:

using Validot;

And you're good to go! At first, create a specification for your model with the fluent api.

Specification<UserModel> 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. As its name stands - it validates objects according to the specification. It's also thread-safe so you can seamlessly register it as a singleton in your DI container.

var validator = Validator.Factory.Create(specification);

Validate the object!

var model = new UserModel(email: "inv@lidv@lue", age: 14);

var result = validator.Validate(model);

The result object contains all information about the errors. Without retriggering the validation process, you can extract the desired form of an output.

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

Features

Advanced fluent API, inline

No more obligatory if-ology around input models or separate classes wrapping just validation logic. Write specifications inline with simple, human-readable fluent API. Native support for properties and fields, structs and classes, nullables, collections, nested members, and possible combinations.

Specification<string> nameSpecification = s => s
    .LengthBetween(5, 50)
    .SingleLine()
    .Rule(name => name.All(char.IsLetterOrDigit));

Specification<string> emailSpecification = s => s
    .Email()
    .And()
    .Rule(email => email.All(char.IsLower))
    .WithMessage("Must contain only lower case characters");

Specification<UserModel> 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");

Validators

Compact, highly optimized, and thread-safe objects to handle the validation.

Specification<BookModel> 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<IValidator<BookModel>>(bookValidator);
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

Results

Whatever you want. Error flag, compact list of codes, or detailed maps of messages and codes. With sugar on top: friendly ToString() printing that contains everything, nicely formatted.

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,
    };
}

Rules

Tons of rules available out of the box. Plus, an easy way to define your own with the full support of Validot internal features like formattable message arguments.

public static IRuleOut<string> ExactLinesCount(this IRuleIn<string> @this, int count)
{
    return @this.RuleTemplate(
        value => value.Split(Environment.NewLine).Length == count,
        "Must contain exactly {count} lines",
        Arg.Number("count", count)
    );
}
.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

Translations

Pass errors directly to the end-users in the language of your application.

Specification<UserModel> 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, Spanish, Russian, Portuguese and German.

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, and use the following snippet:

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 ...
}

Validot vs FluentValidation

A short statement to start with - @JeremySkinner's FluentValidation 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, 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 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.

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, 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.

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. 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.

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

FluentValidation's IsValid is a property that wraps a simple check whether the validation result contains errors or not. Validot has AnyErrors that acts the same way, and 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 - 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 quickly verifies the object, and if it contains errors - only then 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:

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:
    • FluentValidation validates model, and ToString() is called if errors are detected.
    • Validot processes the model twice - at first, with its special mode, 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. You don't need to secure the validation logic from null as your predicate will never receive it.

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.

If null should be allowed, place Optional command at the beginning:

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.

Validot's Validator is immutable

Once validator instance is created, you can't modify its internal state or settings. If you need the process to fail fast (FluentValidation's CascadeMode.Stop), use the flag:

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:

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:
  • Access to any stateful context in the rule condition predicate:
    • It implicates a lack of support for dynamic message content and/or amount.
  • Callbacks:
  • 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)
  • Rule sets
    • workaround; multiple validators for different parts of the object.
  • await/async support
  • severities (more details on GitHub Issues)
    • workaround: multiple validators 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 that lists all the platforms that can use it on.

Versioning

Semantic versioning 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.

Before publishing, each release is tested on the "latest" version of the following operating systems:

  • macOS
  • Ubuntu
  • Windows Server

using the upcoming, the current and all also the supported LTS versions 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 based on BenchmarkDotNet. Also, you can trigger performance tests from the build script.

Documentation

The documentation is hosted alongside the source code, in the git repository, as a single markdown file: DOCUMENTATION.md.

Code examples from the documentation live as functional tests.

Development

The entire project (source code, issue tracker, documentation, and CI workflows) 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 file and issues page.

Licencing

Validot uses the MIT license. Long story short; you are more than welcome to use it anywhere you like, completely free of charge and without oppressive obligations.

validot's People

Contributors

bartoszlenar avatar blenar-neurosys avatar jeremyskinner avatar restebanlm avatar simoncropp avatar twentyfourminutes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

validot's Issues

Consider extensibility point for extensions which accept `Specification<T>`

Feature description

  • As for now there's only one extensibility point: custom rules - which must use RuleTemplate, but it doesn't support to accept Specification<T> directly. So it's not possible to write extension like built-in AsNullable, AsCollection, etc.
  • Problem: if you have some kind of wrapper which not covered by built-in extensions (e.g. AsNullable), you forced to repeat boilerplate code
Example

Assume we have own Option<T> instead of Nullable<T> (other example - primitive obsession and it's solution like https://github.com/andrewlock/StronglyTypedId):

public readonly record struct Option<TValue>
    where TValue : notnull
{
    private readonly TValue? val;

    internal Option(bool isSome, TValue? value = default)
    {
        IsSome = isSome;

        val = value;
    }

    public bool IsSome { get; }
    public bool IsNone => !IsSome;

    public bool Some([MaybeNullWhen(false)] out TValue value)
    {
        value = val;
        return IsSome;
    }
}

When validating we should have same logic as AsNullable:

  • Ok if Option.IsSome == false;
  • Validate Option's inner value otherwise.

We'd like to add extension AsOption<TValue>(this IRuleIn<Option<TValue>> @this, Specification<TValue> specification), but currently it's not possible to do something like that. So you forced to repeat something like

Specification<Option<int>> s => s
    .Rule(o => o.IsSome)
    .Rule(o => o.Some(out var value) && ...)
;

Feature details

  • Same as built-in rules, built-in wrappers like AsNullable must be migrated to this extensibility point.
  • One of the possible solution: make all of the internal ICommand interfaces public alongside with AddCommand method.
  • There's bunch of ((SpecificationApi<T>)@this).AddCommand(...) downcast, fluent builder should be updated to allow new functionality without such crutches.
  • Caveat: it's possible to make it harder to add new functional in the future (e.g. #2)

WithFailFastStrategy - fail fast, but only in the related scope

Feature description

  • Ability to set the fail-fast strategy in a single scope.

Feature in action

Specification<Model> specification = s => s
    .Member(m => m.Member, memberSpecification).WithFailFastStrategy();
Specification<Model> specification = s => s
    .AsModel(anotherSpecification).WithFailFastStrategy();

Feature details

  • New parameter command: WithFailFastStrategy.
  • The related scope command would terminate and return error output immediately after detecting a single error.
  • Internally it looks like temporary setting FailFast to true in the ValidationContext class, but let's investigate that further.

ASP.Net Core integration/automatic IOC Container resolver

Feature description

As stated in the README:

Integration with ASP.NET or other frameworks:
Making such a thing wouldn't be a difficult task at all, but Validot tries to remain a single-purpose library, and all integrations need to be done individually.

I totally understand that and I agree with that, however as I am currently trying to replace FluentValidation with Validot I find it especially tedious to register all of those validators by hand and usually I use methods which automatically observe all the types/validators.

IMHO it is very important to keep things as simple as possible, in order to make a package as accessible as possible. This however, creates some kind of obstacle, at least to some degree. Especially due to the support for ASP.Net Core by FluentValidation.

To keep all worlds happy I'd suggest a second nuget package which helps solving that. On the one hand the core library is still totally independent and kept nice and tidy and on the other hand it is more user friendly.

Feature in action

Depending on the supported IOC Containers you would have something like the following.

services.AddValidators<Type>(); // The type would be in an assembly with all the other validators.
// Maybe with an overload to add multiple assemblies at once.
services.AddValidators(typeof(ValidatorA).Assembly, typoef(ValidatorB).Assembly);

Feature details

However this would raise the question, which kind of pattern would be used for this. I personally would have a few ideas, but would love to hear other ideas.

  1. A static class containing all the validators
public static class Validators
{
   public static Specification<Type> ValidatorA = ...;
   // Omitted for brevity
}

Pros
It keeps things nice and tidy and you don't need to create a mess with dozen of classes.
Cons
As I am coming from a DDD background I strongly dislike this approach since a lot of these validators wouldn't belong in one class. Additionally this would be hard to observe and still would require some kind of naming convention or similar.

  1. Classes with properties enforced through interfaces
public class ValidatorA : IValidator
{
   public Specification<Type> Validator => ...;
   // Omitted for brevity
}

Pros
It would be more DDD conform and it would be way easier to observe with reflection.
Cons
This would obviously force the user to create a class for each validator which can lead to a lot of boilerplate.

Discussion

Now would you guys even be up to support/endorse something like that? Other implementation ideas?

Nullable reference types support

.NET slowly transisions to code that uses nullable reference types. It would be great if Validot supports nullable reference types.

Problem:

public record Model
{
	public string? Language { get; init; }
}

public sealed class ModelValidator : ISpecificationHolder<Model>
{
	public Specification<Model> Specification { get; }

	public ModelValidator()
	{
		Specification = m => m
			.Member(x => x.Language!, language => language // must use a null forgiving operator to use the IsLanguageCode extension 
				.IsLanguageCode()
			);
	}
}

public static IRuleOut<string> IsLanguageCode(this IRuleIn<string> @this) // string doesn't allow a null
{
	// some code
}

I can replace IRuleIn<string> with IRuleIn<string?> but "If the value entering the scope is null, scope commands are not executed." (DOCUMENTATION.md#null-policy) and using null forgiving operator is for me awful.

Nullable support in .NET Standard 2.0

Nullable package can be used to add nullable reference types to .NET Standard and/or a multi-target package can be created (I've never checked how e.g. .NET 6 handles nullable annotations from .NET Standard 2.0 package because I've always used multi-targeted packages so this need verification).

 <PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
    <Nullable>annotations</Nullable>
  </PropertyGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'">
    <PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
  </ItemGroup>

AsChild scope command - validate as child type

Feature description

  • Validating parent type as one of child type.
  • Same feature in FluentValidation.

Feature in action

// assume FirstChildType and SecondChildType both inherited from ParentType

Specification<FirstChildType> firstChildTypeSpecification = s => s;

Specification<SecondChildType> secondChildTypeSpecification = s => s;

Specification<ParentType> parentType = s => s
    .Rule(...) // executed for both "branches"
    .AsChild<FirstChildType>(firstChildTypeSpecification) // only if validated entity is actually FirstChildType
    .AsChild<SecondChildType>(secondChildTypeSpecification); // only if validated entity is actually SecondChildType

Feature details

  • New scope command: AsChild<TChild>.
  • Works under hood as AsModel (?) + condition check.
  • Should extend rules defined at parent level, not override.
  • Should be executed after parent level rules (?).

Discussion

  • Maybe different naming to point exact purpose of this extension?
  • Allow only (abstract) classes or interfaces as well?

IValidot interface

It would be useful to have a IValidator interface with object based IsValid and Validate methods. Also the new interface can expose a SpecifiedType property to easily access the type the validator is validating. This new interface will allow a non-generic code (e.g. in ASP.NET Core) to easily validate models.

    public interface IValidator<T> : IValidator
    {
        /// <summary>
        /// Quickly verifies whether the model is valid (according to the specification) or not.
        /// This is highly-optimized version of <see cref="IValidator{T}.Validate"/>, but it doesn't return any information about errors.
        /// </summary>
        /// <param name="model">The model to be validated.</param>
        /// <returns>True, if model is valid and there are no errors according to the specification. Otherwise - false.</returns>
        bool IsValid(T model);

        /// <summary>
        /// Validates the model against the specification. Returns <see cref="IValidationResult"/> object that contains full information about the errors found during the validation process.
        /// WARNING! The returned <see cref="IValidationResult"/> object is internally coupled with the instance of <see cref="IValidator{T}"/> 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 <see cref="IValidationResult"/> itself or pass it around your system too much.
        /// </summary>
        /// <param name="model">The model to be validated.</param>
        /// <param name="failFast">If true, the validation process will stop after detecting the first error. Otherwise, full validation is performed.</param>
        /// <returns>Full information (in a form of <see cref="IValidationResult"/> about the errors found during the validation process, their location, messages and codes.</returns>
        IValidationResult Validate(T model, bool failFast = false);
    }

    public interface IValidator
    {
        /// <summary>
        /// Gets the type that is validated by the validator. It's T from IValidator{T}.
        /// </summary>
        Type SpecifiedType { get; }

        /// <summary>
        /// Gets settings of this <see cref="IValidator{T}"/> instance.
        /// </summary>
        IValidatorSettings Settings { get; }

        /// <summary>
        /// Gets the validation result that contains all possible paths and errors described in the specification.
        /// It's the specification in a form of <see cref="IValidationResult"/>.
        /// 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'.
        /// </summary>
        IValidationResult Template { get; }

        /// <summary>
        /// Quickly verifies whether the model is valid (according to the specification) or not.
        /// This is highly-optimized version of <see cref="IValidator{T}.Validate"/>, but it doesn't return any information about errors.
        /// </summary>
        /// <param name="model">The model to be validated.</param>
        /// <returns>True, if model is valid and there are no errors according to the specification. Otherwise - false.</returns>
        bool IsValid(object model);

        /// <summary>
        /// Validates the model against the specification. Returns <see cref="IValidationResult"/> object that contains full information about the errors found during the validation process.
        /// WARNING! The returned <see cref="IValidationResult"/> object is internally coupled with the instance of <see cref="IValidator{T}"/> 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 <see cref="IValidationResult"/> itself or pass it around your system too much.
        /// </summary>
        /// <param name="model">The model to be validated.</param>
        /// <param name="failFast">If true, the validation process will stop after detecting the first error. Otherwise, full validation is performed.</param>
        /// <returns>Full information (in a form of <see cref="IValidationResult"/> about the errors found during the validation process, their location, messages and codes.</returns>
        IValidationResult Validate(object model, bool failFast = false);
    }

Add French translation

Feature description

  • French translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithFrenchTranslation()
);

validator.Settings.Translations["French"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "French") // gets result error output in French

Feature implementation walkthrough

  • In Validot, even English language is implemented as a translation that maps the property names from MessageKey class to the human-readable phrases.
  • Take a look at the files in src/Validot/Translations directory and see how the other languages are implemented.
  • In the project's root directory execute pwsh build.ps1 --target AddTranslation --translationName French
  • There is also build.sh if you don't have pwsh installed.
  • You'll notice a the new directory inside src/Validot/Translations that contains two files.
  • Open FrenchTranslation.cs and translate the English phrases.
  • Unit tests are in the related directory tests/Validot.Tests.Unit/Translations, but you don't need to do anything there.
    • Their role is to check whether the translation dictionary contains only the keys from the MessageKey class.
    • You don't need to include all keys translated, but if you want to skip something, please comment the line out instead of removing it (later it would be easier to check what's missing).
  • Done! Now you can make a PR! Thank you for your contribution!

Visually separating rules in specification

Feature description

  • Specifications could be long and have nested inline specifications within. It looks very messy, sometimes even unreadable after a code cleanup (done by e.g., resharper).
  • Would be great to have something that could help with visually organizing the rules in the specification.

Feature in action

First variant; a property.

Specification<BookModel> bookSpecification = s => s
    .Optional()
    .And
    .Member(m => m.AuthorEmail, m => m.Optional().Email())
    .WithMessage("Test message")
    .WithExtraCode("Test code")
    .And
    .Member(m => m.Title, m => m.NotEmpty().LengthBetween(1, 100))
    .WithMessage("Test message")
    .WithExtraCode("Test code")
    .And
    .Member(m => m.Price, m => m.NonNegative());

Looks nice and cannot be taken as the rule or any part of the specification, because those are always functions. The flaw is that technically it needs to be available from all places. So this is legal:

Specification<BookModel> bookSpecification = s => s
    .Optional().And.And
    .And.And
    .Member(m => m.AuthorEmail, m => m.And.Optional().And.Email().And.And.And)
    .And.And
    .WithMessage("Test message")
    .And.And
    .And.And
    .WithExtraCode("Test code")
    .And.And
    .Member(m => m.Title, m => m.And.NotEmpty().And.LengthBetween(1, 100).And.And.And.And.And.And.And)
    .And.And.And.And.And.And.And
    .Member(m => m.Price, m => m.And.And.And.And.And.NonNegative().And.And.And.And)
    .And.And.And.And.And;

Second variant: a method:

Specification<BookModel> bookSpecification = s => s
    .Optional()
    .And()
    .Member(m => m.AuthorEmail, m => m.Optional().Email())
    .WithMessage("Test message")
    .WithExtraCode("Test code")
    .And()
    .Member(m => m.Title, m => m.NotEmpty().LengthBetween(1, 100))
    .WithMessage("Test message")
    .WithExtraCode("Test code")
    .And()
    .Member(m => m.Price, m => m.NonNegative());

Flaw; it might be mistaken for a rule (probably #8 needs to be finished first).
Advantage; fluent-api controls where it's allowed to place And()

Feature details

  • Property variant; a property in the specification interface.
  • Method variant: extension method, along with fluent api logic (e.g., it can be followed only by the rule commands)

AsType scope command - validate as different type

Feature description

Feature in action

Full custom option:

Specification<CustomType> customTypeSpecification = s => s;

TryConvertDelegate<int, CustomType> intToCustomType = (int input, out CustomType output) =>
{
    if (input < 0)
    {
        output = null;
        return false;
    }

    output = new CustomType(input);

    return true;
};

Specification<int> specification = s => s
    .AsType<CustomType>(customTypeSpecification)
    .WithConversion(intToCustomType) // might be required for custom conversion
    .WithConversionErrorMessage("Conversion error!") // optional, if not present, message would be taken from dict;

Predefined options:

Specification<int> specification = s => s
    .AsString(stringSpecification);

Specification<string> specification = s => s
    .AsInt(intSpecification)
    .WithConversionErrorMessage("Invalid string as int!");

Feature details

  • New scope command: AsType<T>.
  • Could (should?) be followed by WithConversion parameter command that would set the conversion logic
    • If not present, throw exception?
    • Enforce WithConversion in fluent api? It is possible if AsType would allow only one following command and NOT ISpecificationOut.
  • WithConversionErrorMessage - optional, setting error message.
  • Plenty of built-in converters. AsInt, AsString, etc.

Invalid argument names in DateTimeOffsetRules, DateTimeRules, and TimeSpanRules

DateTimeOffsetRules, DateTimeRules, and TimeSpanRules use min and max argument names but translations use value as a placeholder.

EXAMPLE:

public static IRuleOut<TimeSpan> GreaterThan(this IRuleIn<TimeSpan> @this, TimeSpan min)
{
return @this.RuleTemplate(m => m > min, MessageKey.TimeSpanType.GreaterThan, Arg.Time(nameof(min), min));
}
public static IRuleOut<TimeSpan?> GreaterThan(this IRuleIn<TimeSpan?> @this, TimeSpan min)
{
return @this.RuleTemplate(m => m.Value > min, MessageKey.TimeSpanType.GreaterThan, Arg.Time(nameof(min), min));
}
public static IRuleOut<TimeSpan> GreaterThanOrEqualTo(this IRuleIn<TimeSpan> @this, TimeSpan min)
{
return @this.RuleTemplate(m => m >= min, MessageKey.TimeSpanType.GreaterThanOrEqualTo, Arg.Time(nameof(min), min));
}
public static IRuleOut<TimeSpan?> GreaterThanOrEqualTo(this IRuleIn<TimeSpan?> @this, TimeSpan min)
{
return @this.RuleTemplate(m => m.Value >= min, MessageKey.TimeSpanType.GreaterThanOrEqualTo, Arg.Time(nameof(min), min));
}
public static IRuleOut<TimeSpan> LessThan(this IRuleIn<TimeSpan> @this, TimeSpan max)
{
return @this.RuleTemplate(m => m < max, MessageKey.TimeSpanType.LessThan, Arg.Time(nameof(max), max));
}
public static IRuleOut<TimeSpan?> LessThan(this IRuleIn<TimeSpan?> @this, TimeSpan max)
{
return @this.RuleTemplate(m => m.Value < max, MessageKey.TimeSpanType.LessThan, Arg.Time(nameof(max), max));
}
public static IRuleOut<TimeSpan> LessThanOrEqualTo(this IRuleIn<TimeSpan> @this, TimeSpan max)
{
return @this.RuleTemplate(m => m <= max, MessageKey.TimeSpanType.LessThanOrEqualTo, Arg.Time(nameof(max), max));
}
public static IRuleOut<TimeSpan?> LessThanOrEqualTo(this IRuleIn<TimeSpan?> @this, TimeSpan max)
{
return @this.RuleTemplate(m => m.Value <= max, MessageKey.TimeSpanType.LessThanOrEqualTo, Arg.Time(nameof(max), max));
}
public static IRuleOut<TimeSpan> Between(this IRuleIn<TimeSpan> @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));
}

[MessageKey.TimeSpanType.NotEqualTo] = "Must not be equal to {value}",
[MessageKey.TimeSpanType.GreaterThan] = "Must be greater than {value}",
[MessageKey.TimeSpanType.GreaterThanOrEqualTo] = "Must be greater than or equal to {value}",
[MessageKey.TimeSpanType.LessThan] = "Must be less than {value}",
[MessageKey.TimeSpanType.LessThanOrEqualTo] = "Must be less than or equal to {value}",

Add Russian translation

Feature description

  • Russian translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithRussianTranslation()
);

validator.Settings.Translations["Russian"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "Russian") // gets result error output in Russian

Feature implementation walkthrough

  • There is a similar feature request (#11) regarding the Spanish language. In the details of that issue I described the full, end-to-end walkthrough how to implement a translation and all the instructions can be applied here as well.

Add Chinese (Mandarin) translation

Feature description

  • Chinese translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithChineseTranslation()
);

validator.Settings.Translations["Chinese"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "Chinese") // gets result error output in Chinese

Feature implementation walkthrough

  • In Validot, even English language is implemented as a translation that maps the property names from MessageKey class to the human-readable phrases.
  • Take a look at the files in src/Validot/Translations directory and see how the other languages are implemented.
  • In the project's root directory execute pwsh build.ps1 --target AddTranslation --translationName Chinese
  • There is also build.sh if you don't have pwsh installed.
  • You'll notice a the new directory inside src/Validot/Translations that contains two files.
  • Open ChineseTranslation.cs and translate the English phrases.
  • Unit tests are in the related directory tests/Validot.Tests.Unit/Translations, but you don't need to do anything there.
    • Their role is to check whether the translation dictionary contains only the keys from the MessageKey class.
    • You don't need to include all keys translated, but if you want to skip something, please comment the line out instead of removing it (later it would be easier to check what's missing).
  • Done! Now you can make a PR! Thank you for your contribution!

async/await support

Feature description

  • Async version of Validate method.
  • Validation is fast enough that I don't believe we need async/await just for sake of having it.
    • Task & thread orchestration would probably take more time than the validation itself.
  • However the async version could be useful when validation tries to handle large collections.

Feature in action

var validator = Validator.Factory.Create(specification, settings => settings
    .WithTaskScheduler(taskScheduler)
)
Specification<object[]> specification = s => s
    .AsCollection(itemSpecification)
    .WithAsyncStrategy(); // will use some default strategy
 // will use the async version only if more than 10k items, validating 100 items in a single task:

Specification<object[]> specification = s => s
    .AsCollection(itemSpecification)
    .WithAsyncStrategy(bulkSize: 100, minCount: 10000);
var result1 = validator.Validate(hugeCollection); // under the hood it triggers async collection validation anyway

var result2 = await validator.Validate(hugeCollection); // same as above, but bubbles up the task?

Feature details

  • Async strategy would be used for collections only (parameter command that affects AsCollection).
  • Task orchestration done with task scheduler set in the settings.
  • ValidationContext would need to be created for each bulk and at the end - all the contexts from all of the bulks would be merged into the master one.
    • At the same time, it would be nice that the original ValidationContext continue with the regular validation in other places.

"Smart names" in the error message

Feature description

  • More human-friendly paths/names argument in the messages.
  • This is pretty much a copy cat of FluentValidation feature.
  • We already have {_name} message argument, maybe create a parameter like {_name|format=friendly} ?
  • SuperValue name would be Super Value..

Feature in action

Specification<decimal> specification = s => s
    .Positive()
    .WithPath("Number.Primary.SuperValue")
    .WithMessage("The {_name|format=friendly} needs to be positive!");

var validator = Validator.Factory.Create(specification);

var result = validator.Validate(-1);

result.ToString();
// Number.Primary.SuperValue: The Super Value needs to be positive!

Feature details

  • Extra parameter in Name argument?
  • Extra method in PathHelper that does the smart path resolving?

Add Spanish translation for messages

Feature description

  • Spanish translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithSpanishTranslation()
);

validator.Settings.Translations["Spanish"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "Spanish") // gets result error output in Spanish

Feature implementation walkthrough

  • In Validot, even English language is implemented as a translation that maps the property names from MessageKey class to the human-readable phrases.
  • Take a look at the files in src/Validot/Translations directory. This is the starting point, where the translation should be placed. Create Spanish directory inside and then SpanishTranslation.cs.
  • The content of SpanishTranslation.cs could be (and should be...) entirely based on the file EnglishTranslation.cs. Please copy-paste the content and replace English phrases with the Spanish ones.
    • Please be aware that the particular phrase could contain a placeholder. If you see the English text like "Must be equal to {value}", don't translate the text between curly brackets { and }. The Polish version for this is "Musi by膰 r贸wne {value}". As you can see, {value} stays as it was.
    • As a matter of fact, it would be nice if you add formatting to your phrases. Placeholders will be filled with the values coming from the rule arguments and you can adjust the culture like this: "Musi by膰 r贸wne {value|culture=pl-PL}". So after the variable name (value) there is a pipeline character | and then parameter (culture) with the value pl-PL - culture code, the string that goes to the CultureInfo.GetCultureInfo method. To truly understand how it works, please read the section about message arguments in the docs
  • Similarly, next to SpanishTranslation.cs, please create SpanishTranslationExtensions.cs and copy-paste the content from the EnglishTranslationExtensions.cs. Just find-and-replace English with Spanish should work just fine.
  • Creating unit tests are as simple as creating extensions from the point above. Enter TranslationTests.cs file and look for nested class English - copy-paste it and replace English with Spanish. You can notice that Polish is done in a similar way.
  • Done! Thank you for your contribution!

Inline code documentation (XML comments)

Feature description

  • Provide inline documentation in a form of XML comments.
  • It's helpful if the user see the short description in the IDE and doesn't need to jump to the online docs.

Feature details

  • Standard XML comments
  • Everything, all public API methods, except the rules (too many of them, and they are simple, short and self-descriptive)

Validated value in the error message

Feature description

  • To have an option to include the validated value in the error message.

Feature in action

m => m.EqualTo(666).WithMessage("The value {_model} is not equal to 666");
validator.Validate(555).ToString();
// The value 555 is not equal to 666

Feature details

  • That would be certainly another layer of creating the messages, {_model} placeholder would be in the cache and replaced with the actual value just during the validition.
  • ValidationContext would perform the operation during the validation.

Questions

That's technically, possible, but why implement this?
Is this really needed?

The fact that it's possible doesn't mean it's easy. And would certainly affect the perfromance.

Add German translation

Feature description

  • German translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithGermanTranslation()
);

validator.Settings.Translations["German"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "German") // gets result error output in German

Feature implementation walkthrough

  • In Validot, even English language is implemented as a translation that maps the property names from MessageKey class to the human-readable phrases.
  • Take a look at the files in src/Validot/Translations directory and see how the other languages are implemented.
  • In the project's root directory execute pwsh build.ps1 --target AddTranslation --translationName German
  • There is also build.sh if you don't have pwsh installed.
  • You'll notice a the new directory inside src/Validot/Translations that contains two files.
  • Open GermanTranslation.cs and translate the English phrases.
  • Unit tests are in the related directory tests/Validot.Tests.Unit/Translations, but you don't need to do anything there.
    • Their role is to check whether the translation dictionary contains only the keys from the MessageKey class.
    • You don't need to include all keys translated, but if you want to skip something, please comment the line out instead of removing it (later it would be easier to check what's missing).
  • Done! Now you can make a PR! Thank you for your contribution!

ErrorOutput

Feature description

  • validationResult.ErrorOutput of type IReadOnlyDictionary<string, IReadOnlyCollection<IErrorOutput>
  • IErrorOutput consists of two props: IReadOnlyCollection<string> Messages and IReadOnlyCollection<string> Codes
  • Basically a map that includes non-merged error outputs (messages and errors) for paths. Non-merged means that it's before squashing distinct errors into single collection like MessageMap and CodeMap. This one keeps a separate error output (IErrorOutput) for each rule in the specification that reported failure.

Feature in action

Specification spec = s => s
.Rule(x => false).WithMessage("Message 11").WithExtraMessage("Message 12").WithExtraCode("Code 11").WithExtraCode("Code 12")
.Rule(x => false).WithMessage("Message 21").WithExtraMessage("Message 22").WithExtraCode("Code 21").WithExtraCode("Code 22")

Two rules at the top level and two entries in ErrorOutput:

result.ErrorOutput["path.to.member"][0].Messages == ["Message 11", "Message 12"]
result.ErrorOutput["path.to.member"][0].Codes == ["Code 11", "Code 12"]

result.ErrorOutput["path.to.member"][1].Messages == ["Message 21", "Message 22"]
result.ErrorOutput["path.to.member"][1].Codes == ["Code 21", "Code 22"]

Feature details

  • Should be using Lazy Loading like ErrorCode and MessageCode.
  • Technically it's _resultErrors but with error details extracted from _errorRegistry

Add support for Dictionary

Feature description

  • To have ability validating dictionary as key-value map, not as collection of key-value pair.

Feature in action

Specification<string> keySpecification = s => s
    .Rule(subj => subj != "foo").WithMessage("Key must not be 'foo'!");

Specification<double> valueSpecification = s => s
    .Rule(subj => subj > 0).WithMessage("Value must be positive!");

Specification<IReadOnlyDictionary<string, double>> specification = s => s
    .AsDictionary(keySpecification, valueSpecification);

var validator = Validator.Factory.Create(specification);

var dictionary = new Dictionary<string, double>{
    ["foo"] = 0,
    ["bar"] = 0,
    ["baz"] = 100
};

validator.Validate(dictionary).ToString();
// #foo: Key must not be 'foo'!
// #foo: Value must be positive! | only if skip is not enabled
// #bar: Value must be positive!

Feature details

  • Validation of Dictionary require validating each TKey, each TValue separately.
  • Both of validation should have access to the full Dictionary to check possible relations (?).
  • Instead of #0 syntax in error messages, #key should be used.

Discussion

  • Consider to add option to skip TValue validation if TKey failed.
  • What to do with complex TKey in #key printing:
    • Just use .ToString()
    • Add WithKey method

Add Japanese translation

Feature description

  • Japanese translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithJapaneseTranslation()
);

validator.Settings.Translations["Japanese"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "Japanese") // gets result error output in Japanese

Feature implementation walkthrough

  • In Validot, even English language is implemented as a translation that maps the property names from MessageKey class to the human-readable phrases.
  • Take a look at the files in src/Validot/Translations directory and see how the other languages are implemented.
  • In the project's root directory execute pwsh build.ps1 --target AddTranslation --translationName Japanese
  • There is also build.sh if you don't have pwsh installed.
  • You'll notice a the new directory inside src/Validot/Translations that contains two files.
  • Open JapaneseTranslation.cs and translate the English phrases.
  • Unit tests are in the related directory tests/Validot.Tests.Unit/Translations, but you don't need to do anything there.
    • Their role is to check whether the translation dictionary contains only the keys from the MessageKey class.
    • You don't need to include all keys translated, but if you want to skip something, please comment the line out instead of removing it (later it would be easier to check what's missing).
  • Done! Now you can make a PR! Thank you for your contribution!

WithSeverity - support for errors severity level

Feature description

  • Add severity support to error output.
    • I'm not sure if it's needed... Maybe it's bringing to much complexity into something that should be fairly simple.
  • Severities could be replaced with... multiple validators.

Feature in action

Specification<string> specification = s => s
    .NotEmpty().WithSeverity(10)
    .NotWhitespace().WithSeverity(0)
Specification<string> specification = s => s
    .NotEmpty().WithSeverity(Severity.High)
    .NotWhitespace().WithSeverity(Severity.Low)
result.AnyErrorsWithSeverity(Severity.High);

result.AnyErrorsWithSeverity(10);

result.GetMessageMapWithSeverity(Severity.Medium | Severity.High)

Feature details

  • New parameter command: WithSeverity.
  • Would be marking the entire error output with a severity level.
  • Result would have methods to filter out error outputs that have certain level of severity...

Add Portuguese translation

Feature description

  • Portuguese translation for the default Validot's message set.
  • Placeholders support where applicable.
  • Fluent api extension method to ValidatorSettings so it can be nicely used from Validator.Factory.Create.

Feature in action

var validator = Validator.Factory.Create(
    specification,
    s => s.WithPortugueseTranslation()
);

validator.Settings.Translations["Portuguese"] // this is the full translation dictionary

validator.Validate(model).ToString(translationName: "Portuguese") // gets result error output in Portuguese

Feature implementation walkthrough

  • In Validot, even English language is implemented as a translation that maps the property names from MessageKey class to the human-readable phrases.
  • Take a look at the files in src/Validot/Translations directory and see how the other languages are implemented.
  • In the project's root directory execute pwsh build.ps1 --target AddTranslation --translationName Portuguese
  • There is also build.sh if you don't have pwsh installed.
  • You'll notice a the new directory inside src/Validot/Translations that contains two files.
  • Open PortugueseTranslation.cs and translate the English phrases.
  • Unit tests are in the related directory tests/Validot.Tests.Unit/Translations, but you don't need to do anything there.
    • Their role is to check whether the translation dictionary contains only the keys from the MessageKey class.
    • You don't need to include all keys translated, but if you want to skip something, please comment the line out instead of removing it (later it would be easier to check what's missing).
  • Done! Now you can make a PR! Thank you for your contribution!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    馃枛 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 馃搳馃搱馃帀

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google 鉂わ笍 Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.