SteveDunn / Vogen

A semi-opinionated library which is a source generator and a code analyser. It Source generates Value Objects
Apache License 2.0
829 stars 45 forks source link

Better support for the custom validation and exceptions. #408

Open dmitriyi-affinity opened 1 year ago

dmitriyi-affinity commented 1 year ago

Describe the feature

Please include the next scenario in your microbenchmarks and help:

[ValueObject(typeof(string))]
public partial struct AccessToken
{
    public static Validation Validate(string? value)
    {
        Guard.IsNullOrEmpty(value); // <-- Existing validation from the CommunityToolkit.Diagnostics nuget
        return Validation.Ok;
    }
}

It's currently impossible to well generate exceptions with argument name specification. VogenValidationException might be not a good default. Please consider also supporting ArgumentException as a first-class citizen for this library.

SteveDunn commented 1 year ago

Thanks for the report @dmitriyi-affinity. The idea of the Validate method is to just return Validation.Ok or Validation.Invalid("[what is wrong with the value")

You can change the type of exception thrown using Configuration, or, as you're doing, just throw your own exception.

The argument name specification isn't really relevant here as the caller doesn't provide the argument; the validation method is either called via the From method, or when the value object is being deserialized..

Apologies if I'm missing something; what problems are you seeing and what changes would make it better?

dmitriyi-affinity commented 1 year ago

The reason exactly is to generate the correct argument exception for the MyTypedKey.From(T value) method. The raised custom exceptions inherited from the ArgumentException should has ArgumentName = "value".

SteveDunn commented 1 year ago

I think what we could do here is, if the custom exception specified is ArgumentException, then pass in value.

clinington commented 6 months ago

Does it have to throw an exception, could the generator be configure to create a Either<CustomErrors, T> From(string value) for example?

SteveDunn commented 4 months ago

I'm looking again at this one. The From method calls Validate if there is one. It doesn't know what exceptions are thrown from this user-supplied method.

The generated code for the From method is:

        /// <summary>
        /// Builds an instance from the provided underlying type.
        /// </summary>
        /// <param name=""value"">The underlying type.</param>
        /// <returns>An instance of this type.</returns>

Do you want it to emit the exceptions that it throws, something like:

    /// <exception cref="InvalidOperationException"></exception>

Or, it could inspect the Validation method that you supply and copy of the exceptions specified in the triple-slash comments...

clinington commented 4 months ago

Could we have something like [ValueObject(typeof(string), UseErrors=typeof(CustomErrors)] or [ValueObject(typeof(string), UseExceptions=true)] (<- assuming this is the default)

SteveDunn commented 4 months ago

I'm about to add TryFrom which will look like bool TryFrom(string input, out Name result). I could also add something like Either<Validation, Name> TryFrom(input).

clinington commented 4 months ago

Yeah that Either<> TryFrom would be great!

SteveDunn commented 4 months ago

It's in the next version, and it's a simple maybe/result named ValueObjectOrError. Hopefully released today.

clinington commented 4 months ago

So I just quickly tried it out:


[ValueObject(typeof(string))]
public partial record struct Name;

public record struct NameError(string message);
public record struct AgeError(string message);

[GenerateOneOf]
public partial class Errors : OneOfBase<NameError, AgeError> { }

public static class Extensions
{
    public static Either<Errors, T> ToEither<T>(
        this ValueObjectOrError<T> validation,
        Func<Vogen.Validation, Errors> onError) 
        => validation.IsSuccess switch
        {
            true => validation.ValueObject,
            false => onError(validation.Error)
        };
}

// Some Method somewhere

Name.TryFrom("jeff") 
    .ToEither(validation => new NameError(validation.ErrorMessage))
    .Match(
        vo => vo,
        errors => throw new("Got errors"));

Seems pretty reasonable!

Obviously I could a lot more granular with the Data property of Vogen.Validation maybe storing some more fine grain NameErrors in there (expecting that NameErrors would become another discriminated union).

Either I could actually store the record struct TooLongError in the data or, just look for the string key "TooLong" and just map that in the onError parameter

SteveDunn commented 4 months ago

Thanks for the feedback @clinington . I can't wait for DU's to be a 1st class citizen!

Can I close this issue now, or is there anything else that you feel needs fixing or improving?

clinington commented 4 months ago

I wrote this blog post on it: https://medium.com/p/7ff4af9d2322

I wrote this extension:


public static class Extensions
{
    public static Validation<TValidationError, T> ToValidationMonad<TValidationError, T>(
        this ValueObjectOrError<T> validation,
        Func<IReadOnlyList<string>, TValidationError> onError) 
        => validation.IsSuccess switch
        {
            true => validation.ValueObject,
            false => onError(validation.Error.Data?.Keys.Cast<string>().ToList() ?? [])
        };
}

If i wanted to fork your repo to add either support internally, where in the code would I put the autogen stuff to create a Validation<TError, TValueObject> Validate method?