LogAppenderRegistry.java

package io.jstach.rainbowgum;

import java.net.URI;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import io.jstach.rainbowgum.LogProperty.Result;
import io.jstach.rainbowgum.LogProperty.ValidatedResult;

/**
 * Register appenders by name. TODO probably can remove this.
 */
sealed interface LogAppenderRegistry permits DefaultAppenderRegistry {

}

final class DefaultAppenderRegistry implements LogAppenderRegistry {

	static List<LogProvider<LogAppender>> appenders(LogConfig config, String routeName) {
		var keyed = config.properties().forKey(LogProperties.ROUTE_APPENDERS_PROPERTY, routeName);
		if (routeName.equals(LogProperties.DEFAULT_NAME)) {
			keyed = keyed.or(LogProperties.APPENDERS_PROPERTY);
		}

		Result<List<String>> result = keyed.ofList();

		if (routeName.equals(LogProperties.DEFAULT_NAME)) {
			result = result.or(() -> addDefaultAppenderNames(config));
		}

		var _r = result;
		/*
		 * Validated as a plain list of names, before any of them is used to build a
		 * LogProvider, so a bad name is reported against this list property itself
		 * (LogProperty.Validator.validateNames throws its own uncaught
		 * ValidationException) - not wrapped/reformatted by Result.map's own
		 * exception-to-rich-error handling, and not deferred until some unrelated
		 * {name}-keyed property (e.g. this appender's own output/encoder) happens to
		 * interpolate it first.
		 */
		List<String> appenderNames = rawValue(result).value().stream().distinct().toList();
		LogProperty.Validator.validateNames(LogAppender.class, "appender", _r, appenderNames);

		List<LogProvider<LogAppender>> appenders = new ArrayList<>();
		for (String appenderName : appenderNames) {
			appenders.add(appender(appenderName)
				.describe("Appender: '" + appenderName + "' from property: " + _r.describe()));
		}
		return appenders;
	}

	private static List<String> addDefaultAppenderNames(LogConfig config) {
		List<String> appenderNames = new ArrayList<>();
		rawValue(config.properties().forKey(LogProperties.FILE_PROPERTY).ofURI()).optional()
			.ifPresent(a -> appenderNames.add(LogAppender.FILE_APPENDER_NAME));
		appenderNames.add(LogAppender.CONSOLE_APPENDER_NAME);
		return appenderNames;
	}

	/*
	 * appenders() (a Missing route-appenders property is a genuine misconfiguration with
	 * no fallback for a non-default route), addDefaultAppenderNames() (an Error on the
	 * optional logging.file.name check must still surface, unwrapped), and
	 * LogAppender.Builder.build()'s own final "genuinely missing, no default" output
	 * fallback all need the raw, unwrapped exception a Missing/Error's own
	 * value()/optional() throws, not the "Validation failed for X:" wrapping a Validator
	 * would add, which would also change the exception's public type away from
	 * PropertyMissingException/PropertyConvertException for callers (see
	 * FileOutputPropertiesTest) that catch those specifically. LogProperty.Result no
	 * longer exposes value()/optional() itself (a bare Result might still be Missing), so
	 * this just narrows to LogProperty.ValidatedResult, true for every concrete Result,
	 * Missing included, without going through a Validator at all.
	 */
	static <T> ValidatedResult<T> rawValue(Result<T> result) {
		return switch (result) {
			case ValidatedResult<T> vr -> vr;
		};
	}

	static LogProvider<LogAppender> appender(String name) {
		return (_n, config) -> {
			if (name.equals(LogAppender.FILE_APPENDER_NAME)) {
				return fileAppender(config);
			}
			if (name.equals(LogAppender.CONSOLE_APPENDER_NAME)) {
				return defaultConsoleAppender(config);
			}
			return LogAppender.builder(name).build().provide(name, config);
		};
	}

	private static LogAppender defaultConsoleAppender(LogConfig config) {
		String name = LogAppender.CONSOLE_APPENDER_NAME;
		return LogAppender.builder(name).outputDefault(LogOutput.ofStandardOut()).build().provide(name, config);
	}

	static LogAppender fileAppender(LogConfig config) {
		final String name = LogAppender.FILE_APPENDER_NAME;
		/*
		 * Unchanged from before the appender-construction refactor unified everything
		 * else onto LogAppender.Builder: LogProperties.FILE_PROPERTY (Spring Boot's
		 * logging.file.name) is chained ahead of the generic
		 * logging.appender.file.output.uri, and LogProperty.mapValue's own
		 * exception-to-rich-property-error wrapping around the URI-to-LogProviderRef
		 * conversion step (normalizeFileUri/LogProviderRef.of) both need the exact Result
		 * plumbing below, not something a plain builder setter can express without losing
		 * "Error converting property..."/"Tried: ..." context real
		 * FileOutputPropertiesTest cases assert on. Only the very last step changes: the
		 * fully-resolved LogOutput is handed to a real LogAppender.Builder (as an
		 * explicit value, so build() never re-resolves it from properties) instead of
		 * bypassing it via AppenderConfig.
		 */
		Result<URI> uriResult = config.properties()
			.forKey(LogProperties.FILE_PROPERTY)
			.or(LogAppender.APPENDER_OUTPUT_PROPERTY, name)
			.ofURI();
		Result<LogProviderRef> refResult = switch (uriResult) {
			case Result.Success<URI> s -> LogProperty.mapValue(s, LogProviderRef.of(normalizeFileUri(s), s.key()));
			case Result.Missing<URI> m -> m.convert();
			case Result.Error<URI> e -> e.convert();
		};
		LogOutput output = rawValue(LogProperty.provideValue(refResult.map(LogOutput::of), name, config)).value();
		return LogAppender.builder(name).output(output).build().provide(name, config);
	}

	/*
	 * Values that already have an explicit scheme (e.g. file:///...) are left as-is since
	 * they are already unambiguous; genuinely malformed values never reach here, since
	 * ofURI() itself already failed them with their original, well understood URI syntax
	 * error before this runs. Only a value that actually came from FILE_PROPERTY (not the
	 * generic, already-unambiguous APPENDER_OUTPUT_PROPERTY fallback) gets this
	 * treatment, checked via the key the value was actually found at, not which property
	 * this lookup started from.
	 */
	private static URI normalizeFileUri(Result.Success<URI> result) {
		URI uri = result.value();
		if (!LogProperties.FILE_PROPERTY.equals(result.key()) || uri.getScheme() != null) {
			return uri;
		}
		return Paths.get(uri.getPath()).toUri();
	}

}