DocumentValue.java

package org.hammer.audio.plugin.document;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Immutable, framework-neutral value model for bounded experiment-document sections.
 *
 * <p>The host owns JSON parsing and serialization. Plugins receive only this data model and cannot
 * request polymorphic object deserialization.
 */
public sealed interface DocumentValue
    permits DocumentValue.ArrayValue,
        DocumentValue.BooleanValue,
        DocumentValue.NullValue,
        DocumentValue.NumberValue,
        DocumentValue.ObjectValue,
        DocumentValue.StringValue {

  /** Create an immutable object value with lexicographically ordered keys. */
  static ObjectValue object(Map<String, ? extends DocumentValue> fields) {
    SortedMap<String, DocumentValue> copy = new TreeMap<>();
    fields.forEach(copy::put);
    return new ObjectValue(copy);
  }

  /** Create an immutable array value. */
  static ArrayValue array(List<? extends DocumentValue> values) {
    return new ArrayValue(new ArrayList<>(values));
  }

  /** Create a string value. */
  static StringValue string(String value) {
    return new StringValue(value);
  }

  /** Create a canonical finite decimal value. */
  static NumberValue number(BigDecimal value) {
    return new NumberValue(value);
  }

  /** Create a boolean value. */
  static BooleanValue bool(boolean value) {
    return new BooleanValue(value);
  }

  /** Return the singleton null value. */
  static NullValue nullValue() {
    return NullValue.INSTANCE;
  }

  /**
   * Immutable object value.
   *
   * @param fields lexicographically ordered immutable field map
   */
  record ObjectValue(Map<String, DocumentValue> fields) implements DocumentValue {

    // Validate and defensively copy all fields.
    public ObjectValue {
      Objects.requireNonNull(fields, "fields");
      SortedMap<String, DocumentValue> copy = new TreeMap<>();
      fields.forEach(
          (key, value) -> {
            Objects.requireNonNull(key, "object field name");
            if (key.isBlank()) {
              throw new IllegalArgumentException("object field name must not be blank");
            }
            copy.put(key, Objects.requireNonNull(value, "object field value"));
          });
      fields = Collections.unmodifiableMap(copy);
    }
  }

  /**
   * Immutable array value.
   *
   * @param values ordered immutable values
   */
  record ArrayValue(List<DocumentValue> values) implements DocumentValue {

    // Validate and defensively copy all values.
    public ArrayValue {
      Objects.requireNonNull(values, "values");
      List<DocumentValue> copy = new ArrayList<>(values.size());
      for (DocumentValue value : values) {
        copy.add(Objects.requireNonNull(value, "array value"));
      }
      values = Collections.unmodifiableList(copy);
    }
  }

  /**
   * Immutable string value.
   *
   * @param value text value
   */
  record StringValue(String value) implements DocumentValue {

    // Validate the string value.
    public StringValue {
      Objects.requireNonNull(value, "value");
    }
  }

  /**
   * Canonical finite decimal value.
   *
   * @param value canonical decimal value
   */
  record NumberValue(BigDecimal value) implements DocumentValue {

    // Normalize equivalent decimal representations.
    public NumberValue {
      Objects.requireNonNull(value, "value");
      value = value.signum() == 0 ? BigDecimal.ZERO : value.stripTrailingZeros();
    }
  }

  /**
   * Immutable boolean value.
   *
   * @param value boolean value
   */
  record BooleanValue(boolean value) implements DocumentValue {
    // Boolean values require no normalization.
  }

  /** Singleton null value. */
  enum NullValue implements DocumentValue {
    /** The only null value instance. */
    INSTANCE
  }
}