SubprojectName.java

package network.ike.workspace;

import java.util.Objects;
import java.util.regex.Pattern;

/**
 * Validated subproject-name value — the workspace.yaml key, the
 * subproject directory name, and the {@code <subproject>} reference
 * in aggregator POMs all share this string (ike-issues#295).
 *
 * <p>Validation matches {@link FeatureName}: ASCII letters, digits,
 * {@code -}, {@code _}, {@code .}; must start with a letter or digit.
 * Filesystem-safe and shell-metacharacter-safe by construction.
 *
 * <p>Single typed entry point so every consumer of a subproject-name
 * argument ({@code ws:add}, {@code ws:remove}, {@code ws:promote},
 * {@code ws:demote}, {@code ws:detach}, {@code ws:attach-*}, etc.)
 * gets identical validation rather than scattering regex literals
 * at call sites — per the compiler-visibility principle.
 */
public final class SubprojectName {

    /**
     * Syntactic validator. Anchored at both ends; rejects anything
     * not matching the documented rule set.
     */
    private static final Pattern VALID =
            Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]*");

    private final String value;

    private SubprojectName(String value) {
        this.value = value;
    }

    /**
     * Validate {@code raw} and wrap it as a {@code SubprojectName}.
     *
     * @param raw the candidate name (typically from a
     *            {@code -Dsubproject=<name>} command-line argument
     *            or a {@code workspace.yaml} key)
     * @return a validated {@code SubprojectName}
     * @throws IllegalArgumentException if {@code raw} is null, empty,
     *                                  or violates any documented rule
     */
    public static SubprojectName of(String raw) {
        if (raw == null || raw.isEmpty()) {
            throw new IllegalArgumentException(
                    "Subproject name must be non-empty.");
        }
        if (!VALID.matcher(raw).matches()) {
            throw new IllegalArgumentException(
                    "Subproject name '" + raw + "' is not filesystem-safe. "
                            + "Allowed: ASCII letters, digits, '-', '_', '.'; "
                            + "must start with a letter or digit; no path "
                            + "separators, whitespace, or shell metacharacters.");
        }
        return new SubprojectName(raw);
    }

    /**
     * The validated subproject name as a string.
     *
     * @return the raw value (never null or empty)
     */
    public String value() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        return (o instanceof SubprojectName other) && value.equals(other.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    @Override
    public String toString() {
        return value;
    }
}