Chainable.java

package com.maybeitssquid.safeascii.internal;

import java.util.function.IntFunction;

/**
 * Abstract base class for creating a chain of character processing functions.
 *
 * <p>This class implements {@link IntFunction} to transform Unicode codepoints into {@link
 * CharSequence}s. Implementations can define specific transformation logic in the {@link
 * #process(int)} method. The default {@link #apply(int)} implementation facilitates chaining by
 * feeding the output of the processing step into a delegate function.
 */
public abstract class Chainable implements IntFunction<CharSequence> {
  /** The upper bound for ASCII codepoints (exclusive). */
  public static final int ASCII = 128;

  /** The next function in the processing chain. */
  protected final IntFunction<CharSequence> delegate;

  /**
   * Creates a new Chainable instance with the specified delegate.
   *
   * @param delegate the next function to apply in the chain
   */
  protected Chainable(final IntFunction<CharSequence> delegate) {
    this.delegate = delegate;
  }

  /**
   * Processes a single codepoint according to the specific logic of this class.
   *
   * @param codepoint the Unicode codepoint to process
   * @return the transformed character sequence
   */
  protected abstract CharSequence process(final int codepoint);

  /**
   * Invokes the delegate function on the given codepoint.
   *
   * @param codepoint the codepoint to pass to the delegate
   * @return the result from the delegate
   */
  protected final CharSequence delegate(final int codepoint) {
    return delegate.apply(codepoint);
  }

  /**
   * Applies this processing step and then the delegate to the input value.
   *
   * <p>This implementation first transforms the input {@code value} using {@link #process(int)}.
   * The resulting {@link CharSequence} is then decomposed into codepoints, each of which is passed
   * to the {@link #delegate(int)} method. The results are concatenated and returned.
   *
   * @param value the input codepoint
   * @return the final processed character sequence
   */
  @Override
  public CharSequence apply(final int value) {
    final CharSequence result = process(value);
    final int len = result.length();
    return switch (len) {
      case 0 -> "";
      case 1 -> delegate(result.charAt(0));
      default -> {
        final StringBuilder builder = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
          if (Character.isLowSurrogate(result.charAt(i))) {
            continue;
          }
          final int nextPoint = Character.codePointAt(result, i);
          final CharSequence chunk = delegate(nextPoint);
          builder.append(chunk);
        }
        yield builder.toString();
      }
    };
  }
}