Json5FormatterBuilder.java

package io.jstach.rainbowgum.format;

import io.jstach.rainbowgum.LogProperties;
import io.jstach.rainbowgum.LogProperty;

/**
 * Builder to create {@link io.jstach.rainbowgum.LogFormatter }.
 * Formats each event as a JSON5 (or JSON) object, one line unless pretty printed in
 * full, for switching an application to structured logging without adding a module,
 * for example:
 * <code>{time:"2026-10-05T15:04:05.123Z",level:"INFO" ,logger:"com.example.App",thread:"main",msg:"hello world",requestId:"42"}</code>.
 * <p>
 * The fields use logfmt's names: <code>time</code> (UTC instant with milliseconds),
 * <code>level</code>, <code>logger</code>, <code>thread</code>, <code>msg</code>, and
 * for a throwable <code>error</code> (its <code>toString()</code>) and
 * <code>stacktrace</code>. Every value, key values included, is a string, or
 * <code>null</code> for a key value without one. For a specific schema (ECS, GELF,
 * Logstash) and more options use the <code>rainbowgum-json</code> module.
 * <p>
 * With JSON5, values can be colored like the TTLL formatter, and by default the level
 * is padded after its closing quote so the value stays the level name:
 * <code>level:"INFO" ,logger:...</code>. With JSON the color, theme, and pretty print
 * are ignored, so the output stays plain JSON.
 * <table class="table">
 * <caption><strong>Properties retrieved from LogProperties</strong></caption>
 * <tr>
 * <th>Property Pattern</th>
 * <th>Type</th>
 * <th>Required</th>
 * <th>Default</th>
 * <th>Description</th>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_format }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>true</code></td>
 * <td>{@link io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format#JSON5 json5}</td>
 * <td>JSON5 (default), which leaves identifier keys unquoted, or JSON.</td>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_keyValues }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>true</code></td>
 * <td>{@link io.jstach.rainbowgum.format.KeyValuesPlacement#MERGED merged}</td>
 * <td>Where key values go, see {@link KeyValuesPlacement}.</td>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_prettyPrint }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>true</code></td>
 * <td>{@link io.jstach.rainbowgum.format.Json5PrettyPrint#LEVEL_PADDING level_padding}</td>
 * <td>JSON5 only, added whitespace, see {@link Json5PrettyPrint}.</td>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_color }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>false</code></td>
 * <td><code>null</code></td>
 * <td>JSON5 only, whether to color values, see {@link TTLL.ColorMode}.</td>
 * </tr>
 * <tr>
 * <td><code>{@value PROPERTY_theme }</code></td>
 * <td><code>String (converted)</code></td>
 * <td><code>false</code></td>
 * <td><code>null</code></td>
 * <td>JSON5 only, which colors, see {@link TTLL.ColorTheme}.</td>
 * </tr>
 * </table>
 */
public final class Json5FormatterBuilder implements io.jstach.rainbowgum.LogBuilder<Json5FormatterBuilder,io.jstach.rainbowgum.LogFormatter> {

	/**
	 * The properties to be retrieved from config will have
	 * this prefix.
	 */
	static final String PROPERTY_PREFIX = "logging.appender.{name}.encoder.json5.";
	
	/**
	 * <code>{@value #PROPERTY_format } = io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format</code> JSON5 (default), which leaves identifier keys unquoted, or JSON.
	 */
	static final String PROPERTY_format = PROPERTY_PREFIX + "format";
	/**
	 * <code>{@value #PROPERTY_keyValues } = io.jstach.rainbowgum.format.KeyValuesPlacement</code> Where key values go, see {@link KeyValuesPlacement}.
	 */
	static final String PROPERTY_keyValues = PROPERTY_PREFIX + "keyValues";
	/**
	 * <code>{@value #PROPERTY_prettyPrint } = io.jstach.rainbowgum.format.Json5PrettyPrint</code> JSON5 only, added whitespace, see {@link Json5PrettyPrint}.
	 */
	static final String PROPERTY_prettyPrint = PROPERTY_PREFIX + "prettyPrint";
	/**
	 * <code>{@value #PROPERTY_color } = io.jstach.rainbowgum.format.TTLL.ColorMode</code> JSON5 only, whether to color values, see {@link TTLL.ColorMode}.
	 */
	static final String PROPERTY_color = PROPERTY_PREFIX + "color";
	/**
	 * <code>{@value #PROPERTY_theme } = io.jstach.rainbowgum.format.TTLL.ColorTheme</code> JSON5 only, which colors, see {@link TTLL.ColorTheme}.
	 */
	static final String PROPERTY_theme = PROPERTY_PREFIX + "theme";
	
	private final String propertyPrefix;
	final String property_format;
	final String property_keyValues;
	final String property_prettyPrint;
	final String property_color;
	final String property_theme;
	
	private final java.lang.String name;
	private io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format format = io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format.JSON5;
	private io.jstach.rainbowgum.format.KeyValuesPlacement keyValues = io.jstach.rainbowgum.format.KeyValuesPlacement.MERGED;
	private io.jstach.rainbowgum.format.Json5PrettyPrint prettyPrint = io.jstach.rainbowgum.format.Json5PrettyPrint.LEVEL_PADDING;
	private io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorMode color = null;
	private io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorTheme theme = null;
	private io.jstach.rainbowgum.@org.jspecify.annotations.Nullable ServiceRegistry serviceRegistry;
	
	/**
	 * Create a builder for {@code io.jstach.rainbowgum.LogFormatter }.
	 * @param name will fill <code>{name}</code> in <code>logging.appender.{name}.encoder.json5.</code>. Encoder name, used for property lookup.
	 */
	public Json5FormatterBuilder(
			java.lang.String name
			) {
		try {
			java.util.Map<String,String> prefixParameters = java.util.Map.of(
					"name", name
				);
			this.propertyPrefix = LogProperties.interpolateKey(PROPERTY_PREFIX, prefixParameters);
			this.name = name;
			property_format = LogProperties.interpolateKey(PROPERTY_format, prefixParameters);
			property_keyValues = LogProperties.interpolateKey(PROPERTY_keyValues, prefixParameters);
			property_prettyPrint = LogProperties.interpolateKey(PROPERTY_prettyPrint, prefixParameters);
			property_color = LogProperties.interpolateKey(PROPERTY_color, prefixParameters);
			property_theme = LogProperties.interpolateKey(PROPERTY_theme, prefixParameters);
		}
		catch (IllegalArgumentException e) {
			throw LogProperty.ValidationException.of(this.getClass(), e);
		}
	}


	/**
	 * Sets serviceRegistry.
	 * Where themes chosen by name in properties are looked up.
	 * Default is <code>null</code>.
	 * @param serviceRegistry not configurable through properties Where themes chosen by name in properties are looked up.
	 * @return this builder.
	 */
	public Json5FormatterBuilder serviceRegistry(io.jstach.rainbowgum.@org.jspecify.annotations.Nullable ServiceRegistry serviceRegistry) {
		this.serviceRegistry = serviceRegistry;
		return this;
	}

	
	/**
	 * Sets <strong>required</strong> format.
	 * JSON5 (default), which leaves identifier keys unquoted, or JSON.
	 * Default is {@link io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format#JSON5 json5}.
	 * @param format <code>{@value #PROPERTY_format } = io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format</code> JSON5 (default), which leaves identifier keys unquoted, or JSON.
	 * @return this builder.
	 */
	public Json5FormatterBuilder format(io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format format) {
		this.format = format;
		return this;
	}
	
	/**
	 * Sets <strong>required</strong> keyValues.
	 * Where key values go, see {@link KeyValuesPlacement}.
	 * Default is {@link io.jstach.rainbowgum.format.KeyValuesPlacement#MERGED merged}.
	 * @param keyValues <code>{@value #PROPERTY_keyValues } = io.jstach.rainbowgum.format.KeyValuesPlacement</code> Where key values go, see {@link KeyValuesPlacement}.
	 * @return this builder.
	 */
	public Json5FormatterBuilder keyValues(io.jstach.rainbowgum.format.KeyValuesPlacement keyValues) {
		this.keyValues = keyValues;
		return this;
	}
	
	/**
	 * Sets <strong>required</strong> prettyPrint.
	 * JSON5 only, added whitespace, see {@link Json5PrettyPrint}.
	 * Default is {@link io.jstach.rainbowgum.format.Json5PrettyPrint#LEVEL_PADDING level_padding}.
	 * @param prettyPrint <code>{@value #PROPERTY_prettyPrint } = io.jstach.rainbowgum.format.Json5PrettyPrint</code> JSON5 only, added whitespace, see {@link Json5PrettyPrint}.
	 * @return this builder.
	 */
	public Json5FormatterBuilder prettyPrint(io.jstach.rainbowgum.format.Json5PrettyPrint prettyPrint) {
		this.prettyPrint = prettyPrint;
		return this;
	}
	
	/**
	 * Sets color.
	 * JSON5 only, whether to color values, see {@link TTLL.ColorMode}.
	 * Default is <code>null</code>.
	 * @param color <code>{@value #PROPERTY_color } = io.jstach.rainbowgum.format.TTLL.ColorMode</code> JSON5 only, whether to color values, see {@link TTLL.ColorMode}.
	 * @return this builder.
	 */
	public Json5FormatterBuilder color(io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorMode color) {
		this.color = color;
		return this;
	}
	
	/**
	 * Sets theme.
	 * JSON5 only, which colors, see {@link TTLL.ColorTheme}.
	 * Default is <code>null</code>.
	 * @param theme <code>{@value #PROPERTY_theme } = io.jstach.rainbowgum.format.TTLL.ColorTheme</code> JSON5 only, which colors, see {@link TTLL.ColorTheme}.
	 * @return this builder.
	 */
	public Json5FormatterBuilder theme(io.jstach.rainbowgum.format.TTLL.@org.jspecify.annotations.Nullable ColorTheme theme) {
		this.theme = theme;
		return this;
	}
	
	/**
	 * Creates {@code io.jstach.rainbowgum.LogFormatter } from this builder.
	 * @return {@code io.jstach.rainbowgum.LogFormatter }.
	 */
	public io.jstach.rainbowgum.LogFormatter build() {
		try {
			return io.jstach.rainbowgum.format.Json5Formatter.of(
					this.name
					, io.jstach.rainbowgum.LogProperty.require(property_format, this.format)
					, io.jstach.rainbowgum.LogProperty.require(property_keyValues, this.keyValues)
					, io.jstach.rainbowgum.LogProperty.require(property_prettyPrint, this.prettyPrint)
					, this.color
					, this.theme
					, this.serviceRegistry
					);
		}
		catch (IllegalArgumentException | LogProperty.PropertyMissingException e) {
			throw LogProperty.ValidationException.of(this.getClass(), e);
		}
	}
	
	@Override
	public Json5FormatterBuilder fromProperties(LogProperties properties) {
		var __v = LogProperty.Validator.of(this.getClass());
		var _format = properties.forKey(property_format).ofString().map(_v -> io.jstach.rainbowgum.format.Json5Formatter.convertFormat(_v)).or(this.format).validate(__v);
		var _keyValues = properties.forKey(property_keyValues).ofString().map(_v -> io.jstach.rainbowgum.format.Json5Formatter.convertKeyValues(_v)).or(this.keyValues).validate(__v);
		var _prettyPrint = properties.forKey(property_prettyPrint).ofString().map(_v -> io.jstach.rainbowgum.format.Json5Formatter.convertPrettyPrint(_v)).or(this.prettyPrint).validate(__v);
		var _color = properties.forKey(property_color).ofString().map(_v -> io.jstach.rainbowgum.format.Json5Formatter.convertColor(_v)).or(this.color).validateIfError(__v);
		var _theme = properties.forKey(property_theme).ofString().map(_v -> io.jstach.rainbowgum.format.Json5Formatter.convertTheme(_v)).or(this.theme).validateIfError(__v);
		__v.validate();
		this.format = _format.value();
		this.keyValues = _keyValues.value();
		this.prettyPrint = _prettyPrint.value();
		this.color = _color.valueOrNull();
		this.theme = _theme.valueOrNull();
		return this;
	}
	
	/**
	 * The interpolated property prefix: {@value #PROPERTY_PREFIX}.
	 * @return resolved prefix which should end with a "<code>.</code>".
	 */
	@Override
	public String propertyPrefix() {
		return this.propertyPrefix;
	}
	
}