Structure of a Modddel

A Modddel typically looks like this :

// 1. Imports and Part statements
import 'package:modddels_annotation_fpdart/modddels_annotation_fpdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'age.modddel.dart';
part 'age.freezed.dart';

// 2. The `@Modddel` Annotation
@Modddel(
  validationSteps: [
    ValidationStep([
      Validation('legal', FailureType<AgeLegalFailure>()),
    ], name: 'Value'),
  ],
)
// 3. The class declaration
class Age extends SingleValueObject<InvalidAge, ValidAge> with _$Age {
  // 4. The private empty constructor
  Age._();

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

  // 6. The validate methods
  @override
  Option<AgeLegalFailure> validateLegal(age) {
    if (age.value < 18) {
      return some(const AgeLegalFailure.minor());
    }
    return none();
  }
}

// 7. The Failure(s)
@freezed
class AgeLegalFailure extends ValueFailure with _$AgeLegalFailure{
  const factory AgeLegalFailure.minor() = _Minor;
}

1. Imports and Part statements

For the Modddels generator to work, you need to import modddels_annotation_fpdart or modddels_annotation_dartz (depending on which package you installed), and add the part statement filename.modddel.dart, where filename is the name of the file.

If you're using freezed for making the failures sealed classes, also import it and add the part statement filename.freezed.dart, where filename is the name of the file.

2. The @Modddel Annotation

The @Modddel annotation is where you can define the validationSteps of your modddel.

Example :

@Modddel(
  validationSteps: [
    ValidationStep([
      Validation('size', FailureType<UsernameSizeFailure>()),
      Validation('characters', FailureType<UsernameCharactersFailure>()),
    ], name: 'Form'),
    ValidationStep([
      Validation('reserved', FailureType<UsernameReservedFailure>()),
    ], name: 'Availability'),
  ],
)

As you can see, inside the validationSteps parameter, you provide a list of ValidationSteps in the order that you want them to be processed.

For each ValidationStep, you provide its name, as well as the list of validations it contains (the order doesn't matter here). The name is mainly used to make the name of the invalid-step union-case class name (Ex : 'Form' → 'InvalidUsernameForm'). It must begin with an uppercase letter, and it must be made of valid dart identifier characters (alphanumeric, underscore and dollar sign).

The name parameter of the ValidationStep is optional. (See this section about its default values for ValueObjects, and this section for Entities).

For each Validation, you provide its name, as well as the failureType. The name is used to make the name of the validation method (Ex : 'size' → 'validateSize'), as well as the name of the failure (Ex : 'size' → 'sizeFailure'). It must begin with a lowercase letter, and it must be made of valid dart identifier characters. The failureType is the type of the failure of the validation. You can either provide it as a typeArg : FailureType<UsernameSizeFailure>(), or you can provide it as a string : FailureType('UsernameSizeFailure') (the latter takes precedence).

It is recommended to provide the Failure class as a typeArg, so that you avoid typos. However, if the Failure class is not available during the code generation, you'll have to provide it as a string. This situation can occur when your failure class is generated by a builder (other than freezed) which runs after the modddels generator.

3. The class declaration

When declaring your modddel class, you should extend the appropriate modddel kind, such as SingleValueObject, MultiValueObject, SimpleEntity ... , and provide two type arguments. The first type argument should be 'Invalid' followed by the modddel name, and the second type argument should be 'Valid' followed by the modddel name.

Your modddel class should also mix in a mixin with the name of your modddel prefixed by _$ (Like freezed).

class Age extends SingleValueObject<InvalidAge, ValidAge> with _$Age {
  // ...

4. The private empty constructor

The modddel should only contain one generative constructor, which should be private and empty. Example : Age._(). You should never use this constructor to create an instance of the Modddel.

5. The factory constructor

The parameters of the factory constructor will be the list of properties the modddel contains. Parameters can be positional or named, optional or required, and can have default values.

The factory constructor should always return the result of calling the mixin's _create static method. This method is generated by the modddels generator, and you should pass to it all the parameters of the factory.

Example :

factory FullName(
    String firstName, {
    required String lastName,
}) {
    // Notice how all the parameters of the `_create` method are named.
    // This is so that you don't mix-up the order of the arguments.
    return _$FullName._create(
      firstName: firstName,
      lastName: lastName,
    );
}

Your factory constructor should be unnamed unless you want to create a union of modddels.

Decorators and comments

You can document/decorate a field by documenting/decorating its parameter inside the factory constructor.

Example : Documenting firstName and annotating lastName with @Deprecated :

factory FullName(
    /// The firstName of the user.
    ///
    /// This will be visible to other users.
    String firstName, {
    @Deprecated('Will be removed') required String lastName,
  })

Asserts & Sanitization

The factory constructor allows you to do all sorts of data manipulation or sanitization before effectively creating the modddel.

Example :

factory FullName(
    String firstName, {
    required String lastName,
}) {
    assert(firstName.isNotEmpty && lastName.isNotEmpty);

    final sanitizedFirstName = firstName.trim();
    final sanitizedLastName = lastName.trim();

    return _$FullName._create(
      firstName: sanitizedFirstName,
      lastName: sanitizedLastName,
    );
}

6. The validate methods

After running the generator, your IDE will prompt you to override the "validate" method(s) :

This is the place where you can implement your validation logic. For example :

@override
Option<AgeLegalFailure> validateLegal(_ValidateAgeLegal age) {
    if (age.value < 18) {
      return some(const AgeLegalFailure.minor());
    }
    return none();
}

The age parameter contains all the fields of the Modddel. You can omit its type for a cleaner look :

@override
Option<AgeLegalFailure> validateLegal(age) {
    ...
}

You should NEVER access an instance member (property, method, getter...) from within the "validate" methods. Doing so will throw a runtime error.

@override
Option<AgeLegalFailure> validateLegal(age) {
    // Bad, will throw a runtime error.
    final val = this.value;
    // Good
    final val = age.value;
}

7. The Failure(s)

In the example, the Failure is created using freezed.

@freezed
class AgeLegalFailure extends ValueFailure with _$AgeLegalFailure{
  const factory AgeLegalFailure.minor() = _Minor;
}

Note that the failure should always extend ValueFailure if the modddel is a ValueObject, and it should extend EntityFailure if the modddel is an Entity.

The name of the ValueFailure sealed class can be anything, but as a good practice it's better to name it '{ModddelName}{ValidationName}Failure' (Ex: NameLengthFailure).