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}