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;
023import java.time.Duration;
024
025import static java.util.Objects.requireNonNull;
026
027/**
028 * Immutable result of one MCP rate-limit acquisition attempt.
029 *
030 * <p>
031 * This is a closed domain; exhaustive switches are supported, and any new alternative requires an explicit reviewed contract amendment.
032 *
033 * @author <a href="https://www.revetkn.com">Mark Allen</a>
034 */
035@ThreadSafe
036public sealed interface McpRateLimitDecision
037                permits McpRateLimitDecision.Allowed, McpRateLimitDecision.Denied {
038        /**
039         * Returns an allowed decision.
040         *
041         * @return an allowed decision
042         */
043        @NonNull
044        static Allowed allowed() {
045                return Allowed.INSTANCE;
046        }
047
048        /**
049         * Returns a denied decision with the minimum suggested retry delay.
050         *
051         * @param retryAfter nonnegative retry delay
052         * @return a denied decision
053         */
054        @NonNull
055        static Denied denied(@NonNull Duration retryAfter) {
056                return new Denied(retryAfter);
057        }
058
059        /**
060         * An allowed acquisition.
061         *
062         * @author <a href="https://www.revetkn.com">Mark Allen</a>
063         */
064        @ThreadSafe
065        public final class Allowed implements McpRateLimitDecision {
066                @NonNull
067                private static final Allowed INSTANCE = new Allowed();
068
069                private Allowed() {
070                }
071
072                /** @return whether the other value is also an allowed decision */
073                @Override
074                public boolean equals(@Nullable Object other) {
075                        return other instanceof Allowed;
076                }
077
078                /** @return value-based hash code */
079                @Override
080                public int hashCode() {
081                        return 0;
082                }
083
084                /** @return safe diagnostic rendering */
085                @Override
086                @NonNull
087                public String toString() {
088                        return "Allowed{}";
089                }
090        }
091
092        /**
093         * A denied acquisition carrying a nonnegative minimum suggested retry
094         * delay.
095         *
096         * @author <a href="https://www.revetkn.com">Mark Allen</a>
097         */
098        @ThreadSafe
099        public final class Denied implements McpRateLimitDecision {
100                @NonNull
101                private final Duration retryAfter;
102
103                /**
104                 * Validates this denied decision.
105                 */
106                private Denied(@NonNull Duration retryAfter) {
107                        this.retryAfter = requireNonNull(retryAfter);
108                        if (retryAfter.isNegative())
109                                throw new IllegalArgumentException("retryAfter must not be negative");
110                }
111
112                /** @return nonnegative minimum suggested retry delay */
113                @NonNull
114                public Duration getRetryAfter() {
115                        return this.retryAfter;
116                }
117
118                /** @return whether this decision has the same suggested retry delay */
119                @Override
120                public boolean equals(@Nullable Object other) {
121                        if (this == other)
122                                return true;
123                        if (!(other instanceof Denied denied))
124                                return false;
125                        return this.retryAfter.equals(denied.retryAfter);
126                }
127
128                /** @return value-based hash code */
129                @Override
130                public int hashCode() {
131                        return this.retryAfter.hashCode();
132                }
133
134                /** @return safe diagnostic rendering */
135                @Override
136                @NonNull
137                public String toString() {
138                        return "Denied{retryAfter=" + this.retryAfter + "}";
139                }
140        }
141}