FeatureName.java
package network.ike.workspace;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Validated feature-name value — the {@code <feature>} portion of a
* branch name like {@code feature/<feature>} and the suffix portion
* of a sibling-clone directory name like
* {@code <workspace>-<feature>} (ike-issues#201, ike-issues#205).
*
* <p>Centralizes the regex enforcing "filesystem-safe" so the
* compiler can police a single typed entry point rather than
* scattered string templates at call sites
* (per the compiler-visibility principle).
*
* <p>Validation rules — syntactic only:
* <ul>
* <li>Non-empty</li>
* <li>No path separators ({@code /}, {@code \})</li>
* <li>No whitespace</li>
* <li>No shell-metacharacter hazards
* ({@code *}, {@code ?}, {@code [}, {@code ]}, {@code "},
* {@code '}, {@code $}, backtick)</li>
* <li>ASCII letters, digits, {@code -}, {@code _}, {@code .};
* must start with a letter or digit</li>
* </ul>
*
* <p>Uniqueness checks (does a sibling directory already exist?) are
* intentionally <em>not</em> in this class — those depend on the
* caller's filesystem context and live in the calling Mojo.
*/
public final class FeatureName {
/**
* 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 FeatureName(String value) {
this.value = value;
}
/**
* Validate {@code raw} and wrap it as a {@code FeatureName}.
*
* @param raw the candidate feature name (typically from a
* {@code -Dfeature=<name>} command-line argument)
* @return a validated {@code FeatureName}
* @throws IllegalArgumentException if {@code raw} is null, empty,
* or violates any documented rule
*/
public static FeatureName of(String raw) {
if (raw == null || raw.isEmpty()) {
throw new IllegalArgumentException(
"Feature name must be non-empty.");
}
if (!VALID.matcher(raw).matches()) {
throw new IllegalArgumentException(
"Feature 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 FeatureName(raw);
}
/**
* The validated feature name as a string.
*
* @return the raw value (never null or empty)
*/
public String value() {
return value;
}
/**
* Compose the sibling-clone directory name for this feature inside
* the given primary workspace.
*
* <p>For {@code primaryWorkspaceName="ike-komet-ws"} and feature
* {@code "reasoner"}, returns {@code "ike-komet-ws-reasoner"}.
*
* <p>This is the single approved place to construct sibling
* directory names — call sites must not concatenate strings
* directly (ike-issues#205).
*
* @param primaryWorkspaceName the primary workspace's directory
* name; must be non-null and non-empty
* @return {@code primaryWorkspaceName + "-" + value()}
* @throws IllegalArgumentException if {@code primaryWorkspaceName}
* is null or empty
*/
public String siblingDirectoryName(String primaryWorkspaceName) {
if (primaryWorkspaceName == null || primaryWorkspaceName.isEmpty()) {
throw new IllegalArgumentException(
"Primary workspace name must be non-empty.");
}
return primaryWorkspaceName + "-" + value;
}
@Override
public boolean equals(Object o) {
return (o instanceof FeatureName other) && value.equals(other.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}