LogProvider.java

package io.jstach.rainbowgum;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

import org.jspecify.annotations.Nullable;

/**
 * A factory that may need config to provide.
 *
 * @param <T> component
 */
@FunctionalInterface
public interface LogProvider<T> {

	/*
	 * TODO maybe make this sealed with sub provider types?
	 */

	/**
	 * Creates the component from config. The component is not always guaranteed to be new
	 * object.
	 * @param name config name of the parent component.
	 * @param config config.
	 * @return component.
	 */
	T provide(String name, LogConfig config);

	/**
	 * Convenience for flattening nullable providers.
	 * @param <U> component
	 * @param provider nullable provider
	 * @param name name of parent component and can be ignored if not needed.
	 * @param config config used to provide if not null.
	 * @return maybe null component.
	 */
	@SuppressWarnings("exports")
	public static <U> @Nullable U provideOrNull(@Nullable LogProvider<U> provider, String name, LogConfig config) {
		if (provider == null) {
			return null;
		}
		return provider.provide(name, config);
	}

	/**
	 * Creates a provider of instance that is already configured.
	 * @param <U> component
	 * @param instance component instance.
	 * @return this.
	 */
	public static <U> LogProvider<U> of(U instance) {
		return (n, c) -> instance;
	}

	/**
	 * Wraps with an error description.
	 * @param description description function passed the name and returns description.
	 * @return wrapped provider.
	 */
	default LogProvider<T> describe(String description) {
		return wrap(this, (k, t) -> t.getMessage() + "\n  ↳ Failure providing " + description + ".");
	}

	/**
	 * Wraps with an error description.
	 * @param description description function passed the name and returns description.
	 * @return wrapped provider.
	 */
	default LogProvider<T> describe(Function<String, String> description) {
		return wrap(this, (k, t) -> t.getMessage() + "\n  ↳ Failure providing " + description.apply(k) + ".");
	}

	private static <U> LogProvider<U> wrap(LogProvider<U> provider, BiFunction<String, Throwable, String> description) {
		return (n, c) -> {
			try {
				return provider.provide(n, c);
			}
			catch (Exception e) {
				@SuppressWarnings("null")
				String desc = description.apply(n, e);
				throw new ProvisionException(desc, rootCause(e));
			}
		};
	}

	/*
	 * describe()'s own message building (above) always copies e.getMessage() verbatim as
	 * this new exception's own prefix, so e is fully redundant with the exception being
	 * thrown here, by construction and unconditionally, not just usually: every bit of
	 * information in e.getMessage() already reappears in the new ProvisionException's own
	 * message. Setting cause to e anyway would make the default stack trace printer print
	 * e's own "Caused by:" block too, repeating everything the outer message already
	 * said. Skip straight to e's own cause instead, so repeated describe() calls (one per
	 * layer of appender/route provisioning) collapse down to the single, fully cumulative
	 * top level message plus one real "Caused by:": the one underlying, non-redundant
	 * failure (e.g. a NotFoundException), with its own distinct stack trace. If e has no
	 * cause of its own, it IS that underlying failure (nothing above it to skip to), so
	 * it stays.
	 */
	private static Throwable rootCause(Throwable e) {
		var cause = e.getCause();
		return cause != null ? cause : e;
	}

	/**
	 * Flattens a list of providers to a single provider of a list.
	 * @param <U> provider type.
	 * @param providers list of providers.
	 * @return provider of list.
	 */
	public static <U> LogProvider<List<U>> flatten(List<LogProvider<U>> providers) {
		return (n, c) -> {
			List<U> list = new ArrayList<>();
			for (var p : providers) {
				list.add(p.provide(n, c));
			}
			return list;
		};
	}

	/**
	 * Thrown if a provider fails and a description needs to be added.
	 */
	public static class ProvisionException extends RuntimeException {

		private static final long serialVersionUID = -1666167238381090332L;

		ProvisionException(String message, Throwable cause) {
			super(message, cause);
		}

	}

}