sizovs / PipelinR

PipelinR is a lightweight command processing pipeline ❍ ⇢ ❍ ⇢ ❍ for your Java awesome app.
https://github.com/sizovs/PipelinR
MIT License
420 stars 59 forks source link

Ability to add pipeline behaviours #25

Closed nealeduncan1 closed 2 years ago

nealeduncan1 commented 2 years ago

I am looking for a way to invoke a pipeline behaviour so that it will automatically perform a common action such as validation, before the main handler is called. Right now I need to manually call new CommandValidator(command) as the first line in my handle(...) method.

For example ( I'm using your Validator helper class. ):


...

    @Override
    public Workpack handle(Command command) {
        new CommandValidator(command);

        var workpack = WorkpackFactory.createWorkpack(command);
        workpack = workpackRepository.save(workpack);
        return workpack;
    }

     class CommandValidator {
        public CommandValidator(Command command){
            new Validator<Command>()
                    .with(() -> command.name, v -> v.isBlank() && v.length() <=20, "name must be between 1 and 20 characters")
                    .with(() -> command.name, v -> nameIsUnique(command.name), "name must be unique")
                    .check(command);
        }

        boolean nameIsUnique(String name){
            return !workpackRepository.existsItemByName(name);
        }
    }

Would it be possible to add the ability to have this type of behaviour as part of the pipeline?

sizovs commented 2 years ago

Hi @nealeduncan1

Right now there are two ways to accomplish that. You can: 1) Create an abstract command handler that will invoke the validator. 2) Create a middleware that will resolve and invoke the validator.

Would that work for you?

nealeduncan1 commented 2 years ago

Hi @sizovs - Option 2 was something I was looking at. Do you have an example of how this might work? I wasn't sure how to 'find' the correct instance of the CommandValidator to invoke it.

sizovs commented 2 years ago

@nealeduncan1

1) If you're using Spring 5+, here is the pseudo-code for the Middleware:

@Component
class ValidationMiddleware implements Command.Middleware {

   ValidationMiddleware(ObjectProvider<CommandValidator> validators){
      this.validators = validators;
    }

    @Override
    public <R, C extends Command<R>> R invoke(C command, Next<R> next) {
        CommandValidator validator = validators.stream().filter(validator -> validator.matches(command)).findFirst();
        validator.validate(command);
        return next.invoke();
    }
}

2) To "match" validator with the command, you can rely on generics and Guava's TypeToken:

interface CommandValidator<C extends Command<R>, R> {
    default boolean matches(C command) {
        TypeToken<C> typeToken = new TypeToken<C>(getClass()) {
        };

        return typeToken.isSupertypeOf(command.getClass());
    }
}

3) Finally, add the middleware to PipelinR

nealeduncan1 commented 2 years ago

Got it working now - thank you! Might be worth adding something similar as an example as I think it's a fairly common use case for validation?

sizovs commented 2 years ago

Good idea. Documented: https://github.com/sizovs/PipelinR/commit/c0746d25940ce6664f3de9cac3e4af1874c43fcd.

Closing the issue.

nealeduncan1 commented 2 years ago

Hi - actually, this isn't working quite right. The matches(...) method is finding the wrong CommandValidator. It appears to 'find' the first CommandValidator, so I don't think the logic in the matches is working as expected? The typeToken is java.lang.Object so I'm not sure if this is an issue?

nealeduncan1 commented 2 years ago

Got it sorted now. I had changed my interface to use local type inference, i.e.:

interface CommandValidator<C extends Command<R>, R> {
    default boolean matches(C command) {
        var typeToken = new TypeToken<>(getClass()) {
        };

        return typeToken.isSupertypeOf(command.getClass());
    }
}

But making it explicit (TypeToken<C>) fixed my issue.