modddels icon indicating copy to clipboard operation
modddels copied to clipboard

Add support for async validations

Open CodingSoot opened this issue 1 year ago • 1 comments

Right now, all validation methods are synchronous, and there is no way to make them async.

@Modddel(
  // ...
)
class Age extends SingleValueObject<InvalidAge, ValidAge> with _$Age {
  Age._();

  factory Age(int value) {
    return _$Age._create(
      value: value,
    );
  }

  // ⚠️ This can't be async
  @override
  Option<AgeLegalFailure> validateLegal(age) {
    if (age.value < 18) {
      return some(const AgeLegalFailure.minor());
    }
    return none();
  }
}

Given that factory constructors can't be async, I only see three options :

Option 1 : Use the builder pattern

We generate a "Builder" that you should instantiate and then call an async method (for example runValidations) to get your modddel instance.

final builder = AgeBuilder(20);

final age = await builder.runValidations();
  • Advantages :
    • Ability to lazily validate a modddel (Although this can be easily accomplished by the user using a package like lazy_evaluation or any other way).
  • Disadvantages :
    • Boilerplate code +++
    • Different syntax for instantiating a modddel with sync validations versus async validations

Option 2 : Initialize the modddel manually

We add an init method to the modddel that the user should call, and its returned value is the initialized modddel.

final age = await Age(20).init();
  • Advantages :
    • Less boilerplate code
  • Disadvantages :
    • ⚠️ RUNTIME ERRORS : If the user forgets to call init or accidentally uses the instance created with the factory constructor (Age(20)) , there will be runtime errors that may be hard to debug
    • Again : different syntax for instantiating a sync modddel versus async

Option 3 : Ditch the factory constructors for async methods

We replace the factory constructors with static methods :

  // Instead of a factory constructor : 
  factory Age(int value) {
    return _$Age._create(
      value: value,
    );
  }

  // We use a static method
  static Age create(int value) {
     // To accompany this change, this static method is no longer private
    // (it never needed to be private since the mixin `_$Age` is private)
    return _$Age.create(
      value: value,
    );
  }

The name of the method should be create for solo modddels, and create{UnionCaseName} for unions (ex for a Weather union : createSunny, createRainy...).

Then, these static methods can easily be made async :

  // Change the return type to a future, and optionally add the async keyword if needed 
  static Future<Age> create(int value) {
    return _$Age.create(
      value: value,
    );
  }

And then for making an instance of the modddel :

// If sync : 
final age = Age.create(20);

// If async :
final age = await Age.create(20);
  • Advantages :
    • Same syntax for instantiating sync and async modddels (you only add await)
    • Solves dart-lang/sdk#9 , because static methods have a return type
    • Devs can freely use factory constructors for other purposes
    • The forwarding method (_$Age.create) has the same name as the static method
  • Disadvantages :
    • BREAKING CHANGE
    • Static methods can't be unnamed like factory constructors (Age.create(20) vs Age(20)).
    • Static methods don't preserve generic type information (so if you have generics, you need to forward them in a verbose way)
    • Usually, you create an instance with a constructor, and not a static method, so this might be a little less elegant

CodingSoot avatar Jun 18 '23 00:06 CodingSoot