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.Instant;
025import java.util.Objects;
026import java.util.Optional;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Immutable result of one MCP subscription authorization check. The carrier is
032 * safe for concurrent access; an application remains responsible for the
033 * thread safety of an opaque application-context object stored in an allowed
034 * result. {@link Allowed} and {@link Denied} are the exhaustive result domain.
035 *
036 * <p>
037 * This is a closed domain; exhaustive switches are supported, and any new alternative requires an explicit reviewed contract amendment.
038 *
039 * @author <a href="https://www.revetkn.com">Mark Allen</a>
040 */
041@ThreadSafe
042public sealed interface McpSubscriptionAuthorization
043                permits McpSubscriptionAuthorization.Allowed,
044                McpSubscriptionAuthorization.Denied {
045        /**
046         * Returns the shared denied authorization result.
047         *
048         * @return shared denied result
049         */
050        @NonNull
051        static Denied deniedInstance() {
052                return Denied.INSTANCE;
053        }
054
055        /**
056         * An allowed subscription authorization with an application-requested
057         * expiration and optional replacement application context.
058         *
059         * @author <a href="https://www.revetkn.com">Mark Allen</a>
060         */
061        @ThreadSafe
062        public final class Allowed implements McpSubscriptionAuthorization {
063                @NonNull
064                private final Instant validUntil;
065                @Nullable
066                private final Object applicationContext;
067
068                /**
069                 * Creates an allowed authorization without an application context.
070                 *
071                 * @param validUntil application-requested authorization expiration
072                 * @return allowed authorization
073                 * @throws NullPointerException if {@code validUntil} is null
074                 */
075                @NonNull
076                public static Allowed fromValidUntil(@NonNull Instant validUntil) {
077                        return new Allowed(validUntil);
078                }
079
080                /**
081                 * Vends a builder primed with an application-requested expiration.
082                 *
083                 * @param validUntil application-requested authorization expiration
084                 * @return allowed-authorization builder
085                 * @throws NullPointerException if {@code validUntil} is null
086                 */
087                @NonNull
088                public static Builder withValidUntil(@NonNull Instant validUntil) {
089                        return new Builder(validUntil);
090                }
091
092                private Allowed(@NonNull Instant validUntil) {
093                        this.validUntil = requireNonNull(validUntil);
094                        this.applicationContext = null;
095                }
096
097                private Allowed(@NonNull Builder builder) {
098                        this.validUntil = requireNonNull(builder).validUntil;
099                        this.applicationContext = builder.applicationContext;
100                }
101
102                /** @return application-requested authorization expiration */
103                @NonNull
104                public Instant getValidUntil() {
105                        return this.validUntil;
106                }
107
108                /**
109                 * Returns replacement application context for subsequent authorization and
110                 * projection work. Absence clears any previous application context.
111                 *
112                 * @return replacement application context, when present
113                 */
114                @NonNull
115                public Optional<@NonNull Object> getApplicationContext() {
116                        return Optional.ofNullable(this.applicationContext);
117                }
118
119                /** @return whether this result contains the same values */
120                @Override
121                public boolean equals(@Nullable Object other) {
122                        if (this == other)
123                                return true;
124                        if (!(other instanceof Allowed allowed))
125                                return false;
126                        return this.validUntil.equals(allowed.validUntil)
127                                        && Objects.equals(this.applicationContext,
128                                        allowed.applicationContext);
129                }
130
131                /** @return value-based hash code */
132                @Override
133                public int hashCode() {
134                        return 31 * this.validUntil.hashCode()
135                                        + Objects.hashCode(this.applicationContext);
136                }
137
138                /** @return diagnostic rendering without application-context data */
139                @Override
140                @NonNull
141                public String toString() {
142                        return "Allowed{validUntil=" + this.validUntil
143                                        + ", applicationContext=<redacted>}";
144                }
145
146                /**
147                 * Builder for immutable allowed subscription authorizations.
148                 * <p>
149                 * This class is intended for use by a single thread.
150                 *
151                 * @author <a href="https://www.revetkn.com">Mark Allen</a>
152                 */
153                @NotThreadSafe
154                public static final class Builder {
155                        @NonNull
156                        private final Instant validUntil;
157                        @Nullable
158                        private Object applicationContext;
159
160                        private Builder(@NonNull Instant validUntil) {
161                                this.validUntil = requireNonNull(validUntil);
162                        }
163
164                        /**
165                         * Replaces the application context. Passing null clears it rather than
166                         * retaining a context from an earlier authorization.
167                         *
168                         * @param applicationContext replacement application context, or null to
169                         *                           clear it
170                         * @return this builder
171                         */
172                        @NonNull
173                        public Builder applicationContext(
174                                        @Nullable Object applicationContext) {
175                                this.applicationContext = applicationContext;
176                                return this;
177                        }
178
179                        /** @return immutable allowed subscription authorization */
180                        @NonNull
181                        public Allowed build() {
182                                return new Allowed(this);
183                        }
184                }
185        }
186
187        /**
188         * A data-free denied subscription authorization.
189         *
190         * @author <a href="https://www.revetkn.com">Mark Allen</a>
191         */
192        @ThreadSafe
193        public final class Denied implements McpSubscriptionAuthorization {
194                @NonNull
195                private static final Denied INSTANCE = new Denied();
196
197                private Denied() {
198                }
199
200                /** @return whether the other value is also a denied authorization */
201                @Override
202                public boolean equals(@Nullable Object other) {
203                        return other instanceof Denied;
204                }
205
206                /** @return value-based hash code */
207                @Override
208                public int hashCode() {
209                        return 0;
210                }
211
212                /** @return safe diagnostic rendering */
213                @Override
214                @NonNull
215                public String toString() {
216                        return "Denied{}";
217                }
218        }
219}