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.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.time.Duration;
025
026import static java.util.Objects.requireNonNull;
027
028/**
029 * Immutable finite configuration for Soklet's built-in in-memory token-bucket
030 * rate limiter. Capacity, refill quantity, and refill interval must all be
031 * explicitly configured. This type has no unlimited or disabled form.
032 *
033 * @author <a href="https://www.revetkn.com">Mark Allen</a>
034 */
035@ThreadSafe
036public final class McpTokenBucketConfig {
037        private final long capacity;
038        private final long refillTokens;
039        @NonNull
040        private final Duration refillInterval;
041
042        /**
043         * Vends a builder primed with the bucket's maximum token capacity.
044         *
045         * @param capacity positive maximum number of tokens
046         * @return token-bucket configuration builder
047         * @throws NullPointerException if {@code capacity} is null
048         */
049        @NonNull
050        public static Builder withCapacity(@NonNull Long capacity) {
051                return new Builder().capacity(capacity);
052        }
053
054        private McpTokenBucketConfig(@NonNull Builder builder) {
055                this.capacity = requirePositive(builder.capacity, "capacity");
056                if (builder.refillTokens == null || builder.refillInterval == null)
057                        throw new IllegalStateException(
058                                        "refillTokens and refillInterval must be explicitly configured");
059                this.refillTokens = requirePositive(builder.refillTokens, "refillTokens");
060                this.refillInterval = requireNonNull(builder.refillInterval,
061                                "refillInterval");
062
063                if (this.refillInterval.isZero() || this.refillInterval.isNegative())
064                        throw new IllegalArgumentException("refillInterval must be positive");
065                try {
066                        if (this.refillInterval.toNanos() <= 0)
067                                throw new IllegalArgumentException(
068                                                "refillInterval must be at least one nanosecond");
069                } catch (ArithmeticException exception) {
070                        throw new IllegalArgumentException(
071                                        "refillInterval must fit in a signed 64-bit nanosecond count",
072                                        exception);
073                }
074        }
075
076        /**
077         * Returns the maximum number of tokens held by one bucket.
078         *
079         * @return positive token capacity
080         */
081        @NonNull
082        public Long getCapacity() {
083                return this.capacity;
084        }
085
086        /**
087         * Returns the number of tokens replenished during one refill interval.
088         *
089         * @return positive refill quantity
090         */
091        @NonNull
092        public Long getRefillTokens() {
093                return this.refillTokens;
094        }
095
096        /**
097         * Returns the refill interval.
098         *
099         * @return positive refill interval
100         */
101        @NonNull
102        public Duration getRefillInterval() {
103                return this.refillInterval;
104        }
105
106        /** @return whether every token-bucket setting is equal */
107        @Override
108        public boolean equals(@Nullable Object other) {
109                return this == other
110                                || other instanceof McpTokenBucketConfig configuration
111                                && this.capacity == configuration.capacity
112                                && this.refillTokens == configuration.refillTokens
113                                && this.refillInterval.equals(configuration.refillInterval);
114        }
115
116        /** @return structural token-bucket configuration hash code */
117        @Override
118        public int hashCode() {
119                int result = Long.hashCode(this.capacity);
120                result = 31 * result + Long.hashCode(this.refillTokens);
121                result = 31 * result + this.refillInterval.hashCode();
122                return result;
123        }
124
125        private static long requirePositive(long value, @NonNull String name) {
126                if (value <= 0)
127                        throw new IllegalArgumentException(name + " must be positive");
128                return value;
129        }
130
131        /**
132         * Single-threaded builder for immutable token-bucket configurations.
133         *
134         * @author <a href="https://www.revetkn.com">Mark Allen</a>
135         */
136        @NotThreadSafe
137        public static final class Builder {
138                private long capacity;
139                @Nullable
140                private Long refillTokens;
141                @Nullable
142                private Duration refillInterval;
143
144                private Builder() {}
145
146                /**
147                 * Sets the maximum number of tokens held by one bucket.
148                 *
149                 * @param capacity positive token capacity
150                 * @return this builder
151                 * @throws NullPointerException if {@code capacity} is null
152                 */
153                @NonNull
154                public Builder capacity(@NonNull Long capacity) {
155                        this.capacity = requireNonNull(capacity);
156                        return this;
157                }
158
159                /**
160                 * Sets the number of tokens replenished during one refill interval.
161                 * This value is required and has no default.
162                 *
163                 * @param refillTokens positive refill quantity
164                 * @return this builder
165                 * @throws NullPointerException if {@code refillTokens} is null
166                 */
167                @NonNull
168                public Builder refillTokens(@NonNull Long refillTokens) {
169                        this.refillTokens = requireNonNull(refillTokens);
170                        return this;
171                }
172
173                /**
174                 * Sets the positive refill interval. This value is required and has no
175                 * default.
176                 *
177                 * @param refillInterval positive refill interval
178                 * @return this builder
179                 * @throws NullPointerException if {@code refillInterval} is null
180                 */
181                @NonNull
182                public Builder refillInterval(@NonNull Duration refillInterval) {
183                        this.refillInterval = requireNonNull(refillInterval);
184                        return this;
185                }
186
187                /**
188                 * Builds an immutable finite configuration.
189                 *
190                 * @return token-bucket configuration
191                 * @throws IllegalStateException if refill quantity or interval is absent
192                 * @throws IllegalArgumentException if a numeric value is not positive or
193                 *                                  the refill interval cannot be represented
194                 *                                  in nanoseconds
195                 */
196                @NonNull
197                public McpTokenBucketConfig build() {
198                        return new McpTokenBucketConfig(this);
199                }
200        }
201}