Ad

Form Validation In Clean Architecture

Input validation is a business logic so we should hide this process in the domain layer. as discussed here

I do it like this

Login validator

class LoginValidator extends Validator {
  String email;
  String password;

  LoginValidator(this.email, this.password);

  @override
  void validate(Function() success, Function(List<Failure>) errors) {
    List<Failure> failures = [];
    if (email.trim().isEmpty) {
      failures.add(const EmailValidationFailure('Email is required'));
    } else if (!validator.isEmail(email)) {
      failures.add(const EmailValidationFailure());
    }
    if (password.trim().isEmpty) {
      failures.add(const PasswordValidationFailure('Password is required'));
    }
    if (failures.isNotEmpty) {
      errors(failures);
    } else {
      success();
    }
  }
}

And I created a failure class for each input field

class EmailValidationFailure extends Failure {
  const EmailValidationFailure([String message = 'Email is invalid'])
      : super(message);
}

class PasswordValidationFailure extends Failure {
  const PasswordValidationFailure([String message = 'Incorrect password'])
      : super(message);
} 

And I use the validator in the use case

class LoginUseCaseInteractor implements LoginUseCaseInputPort {
  final AccountRepository _repository;
  final LoginUseCaseOutputPort outputPort;

  LoginUseCaseInteractor(this._repository, this.outputPort);

  @override
  void login(LoginParams params) {
    LoginValidator(params.email, params.password).validate(() async {
      outputPort.loading();
      Result<bool> result = await _repository.login(params);
      result.when(
        success: (data) {
          outputPort.success();
        },
        error: (error) {
          outputPort.requestError(error);
        },
      );
    }, (errors) {
      outputPort.formValidationErrors(errors);
    });
  }
}

and finally I handle the presentation logic in the presenter which it implements the output port of the login use case

class LoginPresenter implements LoginUseCaseOutputPort {
  Reader read;

  LoginPresenter(this.read) : super(LoginState.initial());

  @override
  void formValidationErrors(List<Failure> errors) =>
      state = LoginState.formValidationErrors(errors);

  @override
  void success() => read(setRootPresenterProvider.notifier).setMainPageAsRoot();

  @override
  void loading() => state = LoginState.loading();

  @override
  void requestError(Failure error) => state = LoginState.requestError(error);
}

What I do I create a failure class for each input field and return failures of all fields in a list and in the presentation logic I check the input failures by type

My Question: What if I have a large form (eg:15 fields), Should I create a failure class for each of them? Is there a better way to handle the validation?

Ad

Answer

My Question: What if I have a large form (eg:15 fields), Should I create a failure class for each of them? Is there a better way to handle the validation?

Every error that can occur must be identifiable. Either through dedicated classes, failure codes or constants. Which one you choose depends on the error context information that you want to provide to clients.

I also wouldn't put messages in the failure object, because it is up to a presenter to choose a presentation for a error type. The way an error is presented to the user highly depends on the user interface. Maybe an error doesn't need a string, it is just presented as an icon or a colored marker and so on. The interactor should not create language specific strings. This is part of the user interface.

In simple cases you might only need an error code or constant like 401 and the presenter converts it to Login failed. Of course you can use a common error object for this cases too.

class Failure { int code; }

In other cases you might want to display a more detailed error message like E-mails under the domain '@somedomain.com' are not allowed to login.. In this cases you should use an error object instead of only a simple code to provide details. E.g.

class LoginDomainFailure { String localPart; String domainName }

A presenter can then use this information to generate a failure string or present the error in some other way, e.g. highlighting the domain name in the input field or whatever you can imagine.

Ad
source: stackoverflow.com
Ad