The NullFailure Annotation

Overview and Advantages

Sometimes, you may want a Modddel to be invalid if one of its member parameters is null.

For this, you may intuitively do something like this :

factory Name({required String? value}){
  //...
}

@override
Option<NameLengthFailure> validateLength(name) {
  // We check if the value is null, and if so we return a failure.
    if(name.value == null){
      return some(NameLengthFailure.empty());
    }
    return none();
}

However, there is a much better way to do it : using the @NullFailure annotation.

factory Name({
  @NullFailure('length', NameLengthFailure.empty()) required String? value,
}) {...}

As you can see, you add the @NullFailure annotation in front of the nullable parameter, in the factory constructor. NullFailure receives two positional parameters : the first one is the name of the validation where you want it to be processed, and the second one is the Failure.

This has many advantages over doing the check inside the validation method :

  • Cleaner validate method :

    The NullFailures of a validation are processed right before the validation method is called. So in this example, if the value field is null, then the 'length' validation will fail and validateLength won't be called. However, if it's not null, only then the validateLength method will be called.

    As a result, the name parameter of the validateLength method holds the non-nullable version of value :

    @override
    Option<NameLengthFailure> validateLength(name) {
      // Notice how `value` is non-nullable here
      final String value = name.value;
    }
  • Non-null param transformation :

    In this example, the 'length' validation is part of a validationStep called "Form". If the Modddel passes this validationStep, it means that value is not null.

    As a result, all the union-cases after InvalidNameForm will hold the non-nullable version of value : String value. We call this the Non-null param transformation.

    final name = Name(value: 'Dash');
    
    name.map(
      invalidForm: (invalidNameForm) {
        // nullable
        final String? value = invalidNameForm.value;
      },
      invalidAvailability: (invalidNameAvailability) {
        // non-nullable
        final String value = invalidNameAvailability.value;
      },
      valid: (validName) {
        // non-nullable
        final String value = validName.value;
      },
    );

For an Entity, a NullFailure annotation can't refer to the contentValidation. So you can't do @NullFailure('content', //...).

Usage

As mentioned above, NullFailure receives two positional parameters : the first one is the name of the validation where you want it to be processed, and the second one is the Failure.

The following is a guide on how to use the @NullFailure annotation depending on the modddel kind.

In a ValueObject / SimpleEntity

You add the annotation in front of the nullable member parameter.

Example 1 : A GeoPoint MultiValueObject

factory GeoPoint({
  @NullFailure('range', GeoPointRangeFailure.noLatitude()) 
  required int? latitude,
  @NullFailure('range', GeoPointRangeFailure.noLongitude()) 
  required int? longitude,
})

Example 2 : A Person SimpleEntity

factory Person({
  @NullFailure('identity', PersonIdentityFailure.unnamed()) 
  required FullName? name,
  required Age age,
})

In an IterableEntity

For IterableEntities, it's not the parameter that is nullable, but rather the modddels it holds.

Example : A TodoList ListEntity

factory TodoList({
  // This is a list of nullable todos
  required List<Todo?> todos,
})

When adding a NullFailure annotation, it means that if the collection contains null at least once, the validation will fail with the specified failure.

In our example, let's add a NullFailure annotation :

factory TodoList({
  @NullFailure('integrity', TodoListIntegrityFailure.containsNull()) 
    required List<Todo?> todos,
}) 

Now, if at least one element of todos is null, the 'integrity' validation will fail, with as a failure TodoListIntegrityFailure.containsNull.

In an Iterable2Entity

The usage is similar to IterableEntity. The only difference is that Iterable2Entities have two modddel types, so you need to specify which modddel type you want to annotate using the maskNb parameter.

Example : A BookMap MapEntity. The TypeTemplate of the MapEntity is "Map<#1,#2>"

factory BookMap({
  // This nullfailure refers to the '#2' mask, which in this case matches 
  //'Book?'
  @NullFailure('bibliography', BookMapBibliographyFailure.hasNoBook(), maskNb: 2)
      required Map<Author, Book?> booksByAuthors,
})