ResultImpl.java

package sprouts.impl;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

final class ResultImpl<V> implements Result<V>
{
	private static final Logger log = LoggerFactory.getLogger(ResultImpl.class);

	private final Class<V>      _type;
	private final List<Problem> _problems;
	@Nullable private final V   _value;


	public ResultImpl( Class<V> type, List<Problem> problems, @Nullable V value ) {
		Objects.requireNonNull(type);
		Objects.requireNonNull(problems);
		_type     = type;
		_problems = problems;
		_value    = value;
	}

	/** {@inheritDoc} */
	@Override public Class<V> type() { return _type; }

	/** {@inheritDoc} */
	@Override public List<Problem> problems() { return _problems; }

	/** {@inheritDoc} */
	@Override
	public Result<V> peekAtProblems( Consumer<List<Problem>> consumer ) {
		Objects.requireNonNull(consumer);
		try {
			consumer.accept(problems());
		} catch ( Exception e ) {
			List<Problem> newProblems = new ArrayList<>(problems());
			newProblems.add( Problem.of(e) );
			/*
				An exception in this the user lambda is most likely completely unwanted,
				but we also do not want to halt the application because of it.
				So let's do two things here to make sure this does not go
				unnoticed:
					1. Log the exception
					2. Add it as a problem.
			*/
			log.error("An exception occurred while peeking at the problems of a result.", e);
			return Result.of( type(), this.orElseNull(), newProblems );
		}
		return this;
	}

	/** {@inheritDoc} */
	@Override
	public Result<V> peekAtEachProblem( Consumer<Problem> consumer ) {
		Objects.requireNonNull(consumer);
		Result<V> result = this;
		for ( Problem problem : problems() ) {
			try {
				consumer.accept(problem);
			} catch ( Exception e ) {
				List<Problem> newProblems = new ArrayList<>(result.problems());
				newProblems.add( Problem.of(e) );
				/*
					An exception in this the user lambda is most likely completely unwanted,
					but we also do not want to halt the application because of it.
					So let's do two things here to make sure this does not go
					unnoticed:
						1. Log the exception
						2. Add it as a problem.
				*/
				log.error("An exception occurred while peeking at the problems of a result.", e);
				result = Result.of( type(), result.orElseNull(), newProblems );
			}
		}
		return result;
	}

	/** {@inheritDoc} */
	@Override
	public @Nullable V orElseNullable( @Nullable V other ) { return _value == null ? other : _value; }

	/** {@inheritDoc} */
	@Override public @Nullable V orElseNull() { return _value; }

	/** {@inheritDoc} */
	@Override
	public Result<V> map( Function<V, V> mapper ) {
		Objects.requireNonNull(mapper);
		if ( !isPresent() )
			return Result.of( type() );

		try {
			V newValue = mapper.apply(Objects.requireNonNull(_value));
			if (newValue == null)
				return Result.of(type(), problems());
			else
				return Result.of(newValue, problems());
		} catch ( Exception e ) {
			List<Problem> newProblems = new ArrayList<>(problems());
			newProblems.add( Problem.of(e) );
			return Result.of( type(), newProblems );
		}
	}

	/** {@inheritDoc} */
	@Override
	public <U> Result<U> mapTo( Class<U> type, Function<V, U> mapper ) {
		Objects.requireNonNull(type);
		Objects.requireNonNull(mapper);
		if ( !isPresent() )
			return Result.of( type, problems() );

		try {
			U newValue = mapper.apply( Objects.requireNonNull(_value) );
			if (newValue == null)
				return Result.of( type );
			else
				return Result.of( newValue, problems() );
		} catch ( Exception e ) {
			List<Problem> newProblems = new ArrayList<>(problems());
			newProblems.add( Problem.of(e) );
			return Result.of( type, newProblems );
		}
	}

	@Override
	public String toString() {
        String value = this.mapTo(String.class, Object::toString).orElse("null");
        String type = ( type() == null ? "?" : type().getSimpleName() );
        if ( type.equals("Object") )
			type = "?";
        if ( type.equals("String") && this.isPresent() )
			value = "\"" + value + "\"";
        return "Result<" + type + ">" + "[" + value + "]";
	}

	@Override
	public int hashCode() {
		return Objects.hash(_type, _value, _problems);
	}

	@Override
	public boolean equals( Object obj ) {
		if ( obj == null ) return false;
		if ( obj == this ) return true;
		if ( obj instanceof Result ) {
			Result<?> other = (Result<?>) obj;
			if ( !Objects.equals(other.type(), _type)         ) return false;
			if ( !Objects.equals(other.problems(), _problems) ) return false;
			return
				Val.equals( other.orElseNull(), _value ); // Arrays are compared with Arrays.equals
		}
		return false;
	}
}