RepositoryTenantIdentity.java

package com.taxonomy.workspace.model;

import java.util.Objects;

/**
 * Exact, reversible identity of one repository/workspace/branch tenant.
 *
 * <p>The length-prefixed representation is collision-free even when repository,
 * workspace or branch identifiers contain separators. Lengths are Unicode code
 * point counts so Java encoding matches PostgreSQL {@code char_length} during
 * migration backfills.</p>
 */
public record RepositoryTenantIdentity(
        String repositoryId,
        String workspaceScope,
        String branch
) {
    public static final String PREFIX = "v2|";
    public static final String CENTRAL_SCOPE = "CENTRAL";
    public static final String WORKSPACE_SCOPE_PREFIX = "WORKSPACE:";
    public static final int MAX_SCOPE_KEY_LENGTH = 1024;

    public RepositoryTenantIdentity {
        repositoryId = requireText(repositoryId, "repositoryId", 255);
        workspaceScope = requireText(workspaceScope, "workspaceScope", 320);
        if (!CENTRAL_SCOPE.equals(workspaceScope)
                && (!workspaceScope.startsWith(WORKSPACE_SCOPE_PREFIX)
                || workspaceScope.length() == WORKSPACE_SCOPE_PREFIX.length())) {
            throw new IllegalArgumentException(
                    "workspaceScope must be CENTRAL or WORKSPACE:<workspaceId>");
        }
        branch = requireText(branch, "branch", 255);
    }

    public String scopeKey() {
        StringBuilder key = new StringBuilder(PREFIX);
        append(key, 'r', repositoryId);
        key.append('|');
        append(key, 's', workspaceScope);
        key.append('|');
        append(key, 'b', branch);
        if (key.codePointCount(0, key.length()) > MAX_SCOPE_KEY_LENGTH) {
            throw new IllegalArgumentException(
                    "Encoded repository tenant scope exceeds "
                            + MAX_SCOPE_KEY_LENGTH + " characters");
        }
        return key.toString();
    }

    public static boolean isEncoded(String value) {
        return value != null && value.startsWith(PREFIX);
    }

    public static RepositoryTenantIdentity parse(String scopeKey) {
        Objects.requireNonNull(scopeKey, "scopeKey");
        if (!scopeKey.startsWith(PREFIX)) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key is not a v2 identity");
        }
        Component repository = component(scopeKey, PREFIX.length(), 'r');
        requireSeparator(scopeKey, repository.nextIndex());
        Component scope = component(scopeKey, repository.nextIndex() + 1, 's');
        requireSeparator(scopeKey, scope.nextIndex());
        Component branch = component(scopeKey, scope.nextIndex() + 1, 'b');
        if (branch.nextIndex() != scopeKey.length()) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key contains trailing data");
        }
        return new RepositoryTenantIdentity(
                repository.value(), scope.value(), branch.value());
    }

    private static void append(StringBuilder target, char label, String value) {
        target.append(label)
                .append(value.codePointCount(0, value.length()))
                .append(':')
                .append(value);
    }

    private static Component component(String value, int start, char expectedLabel) {
        if (start >= value.length() || value.charAt(start) != expectedLabel) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key is missing component " + expectedLabel);
        }
        int colon = value.indexOf(':', start + 1);
        if (colon < 0) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key component " + expectedLabel
                            + " has no length delimiter");
        }
        int codePoints;
        try {
            codePoints = Integer.parseInt(value.substring(start + 1, colon));
        } catch (NumberFormatException error) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key component " + expectedLabel
                            + " has an invalid length",
                    error);
        }
        if (codePoints < 1) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key component " + expectedLabel + " is empty");
        }
        int valueStart = colon + 1;
        final int valueEnd;
        try {
            valueEnd = value.offsetByCodePoints(valueStart, codePoints);
        } catch (IndexOutOfBoundsException error) {
            throw new IllegalArgumentException(
                    "Repository tenant scope key component " + expectedLabel + " is truncated",
                    error);
        }
        return new Component(value.substring(valueStart, valueEnd), valueEnd);
    }

    private static void requireSeparator(String value, int index) {
        if (index >= value.length() || value.charAt(index) != '|') {
            throw new IllegalArgumentException(
                    "Repository tenant scope key has an invalid component boundary");
        }
    }

    private static String requireText(String value, String label, int maximumCodePoints) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(label + " must not be blank");
        }
        String normalized = value.strip();
        if (normalized.codePointCount(0, normalized.length()) > maximumCodePoints) {
            throw new IllegalArgumentException(
                    label + " exceeds " + maximumCodePoints + " characters");
        }
        return normalized;
    }

    private record Component(String value, int nextIndex) {
    }
}