Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for the custom validation and exceptions. #408

Open
dmitriyi-affinity opened this issue May 13, 2023 · 13 comments
Open

Better support for the custom validation and exceptions. #408

dmitriyi-affinity opened this issue May 13, 2023 · 13 comments
Labels
enhancement New feature or request

Comments

@dmitriyi-affinity
Copy link

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.

@dmitriyi-affinity dmitriyi-affinity added the enhancement New feature or request label May 13, 2023
@SteveDunn
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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

@clinington
Copy link

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

@SteveDunn
Copy link
Owner

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
Copy link

clinington commented May 3, 2024

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

@SteveDunn
Copy link
Owner

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
Copy link

Yeah that Either<> TryFrom would be great!

@SteveDunn
Copy link
Owner

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

@clinington
Copy link

clinington commented May 10, 2024

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
Copy link
Owner

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
Copy link

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?

@SteveDunn
Copy link
Owner

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?

Thanks for the comment @clinington - apologies for the delay; I was closing some old issues and noticed that I hadn't seen this comment.

The generated call to any validation method supplied by the user is at https://github.com/SteveDunn/Vogen/blob/5.0.6-beta.2/src/Vogen/Util.cs#L71

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants