001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.ThreadSafe;
023
024import static java.util.Objects.requireNonNull;
025
026/**
027 * Immutable, non-secret application identity for one localization catalog
028 * snapshot.
029 * <p>
030 * A revision supports deployment comparison and cache/invalidation
031 * bookkeeping. It is not an MCP wire value, metric label, log field,
032 * historical-catalog request, or distributed-session identifier.
033 *
034 * @author <a href="https://www.revetkn.com">Mark Allen</a>
035 */
036@ThreadSafe
037public final class McpLocalizationRevision {
038        private static final int MAXIMUM_VALUE_BYTES = 128;
039        @NonNull
040        private final String value;
041
042        /**
043         * Creates a revision from its bounded application value.
044         *
045         * @param value 1-128 bytes of visible ASCII
046         * @return immutable revision
047         * @throws NullPointerException if {@code value} is null
048         * @throws IllegalArgumentException if {@code value} is empty, longer than
049         * 128 bytes, or contains a character outside visible ASCII
050         */
051        @NonNull
052        public static McpLocalizationRevision fromValue(@NonNull String value) {
053                return new McpLocalizationRevision(value);
054        }
055
056        private McpLocalizationRevision(@NonNull String value) {
057                this.value = requireNonNull(value);
058                if (value.isEmpty() || value.length() > MAXIMUM_VALUE_BYTES)
059                        throw new IllegalArgumentException(
060                                        "MCP localization revisions must contain 1 through 128 visible ASCII bytes.");
061                for (int index = 0; index < value.length(); ++index) {
062                        char character = value.charAt(index);
063                        if (character < 0x21 || character > 0x7E)
064                                throw new IllegalArgumentException(
065                                                "MCP localization revisions must contain 1 through 128 visible ASCII bytes.");
066                }
067        }
068
069        /** @return application-defined revision value */
070        @NonNull
071        public String getValue() {
072                return this.value;
073        }
074
075        /** @return whether this value has the same application revision */
076        @Override
077        public boolean equals(@Nullable Object other) {
078                if (this == other)
079                        return true;
080                if (!(other instanceof McpLocalizationRevision revision))
081                        return false;
082                return this.value.equals(revision.value);
083        }
084
085        /** @return value-based hash code */
086        @Override
087        public int hashCode() {
088                return this.value.hashCode();
089        }
090
091        /** @return redacted diagnostic rendering */
092        @Override
093        @NonNull
094        public String toString() {
095                return "McpLocalizationRevision{value=<redacted>}";
096        }
097}