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.Instant;
024import java.util.Collections;
025import java.util.EnumSet;
026import java.util.Objects;
027import java.util.Set;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Immutable HTTP admission decision for session-enabled {@code 2025-06-18}
033 * and {@code 2025-11-25} GET delivery or DELETE cleanup. Soklet's
034 * {@code 2026-07-28} implementation does not use this decision.
035 * <p>
036 * Every acceptance supplies an explicit expiry. Soklet caps GET leases using
037 * its configured authorization, subscription and session lifetimes, and
038 * verifies expiry again before delivery or DELETE retirement. Application
039 * values reachable through the identity retain their own thread-safety and
040 * lifecycle contracts.
041 *
042 * <p>
043 * This is a closed domain; exhaustive switches are supported, and any new alternative requires an explicit reviewed contract amendment.
044 *
045 * @author <a href="https://www.revetkn.com">Mark Allen</a>
046 */
047@ThreadSafe
048public sealed interface McpSessionTransportAdmissionDecision
049                permits McpSessionTransportAdmissionDecision.Accepted,
050                McpSessionTransportAdmissionDecision.Rejected {
051        /**
052         * Creates an accepted decision. The selected notification families must be
053         * a subset of the context's offered families; DELETE requires an empty set.
054         * The expiry must still be in the future when Soklet applies the decision.
055         * These operation-dependent conditions are checked by the runtime.
056         *
057         * @param admissionIdentity freshly admitted identity
058         * @param validUntil application authorization expiry
059         * @param notificationTypes selected notification families, possibly empty
060         * @return immutable accepted decision
061         * @throws NullPointerException if an argument or notification type is null
062         */
063        @NonNull
064        static Accepted accepted(@NonNull McpAdmissionIdentity admissionIdentity,
065                        @NonNull Instant validUntil,
066                        @NonNull Set<@NonNull McpSubscriptionNotificationType> notificationTypes) {
067                return new Accepted(admissionIdentity, validUntil, notificationTypes);
068        }
069
070        /**
071         * Creates an HTTP rejection using the existing rejection's safe status and
072         * response headers. GET and DELETE have no JSON-RPC request ID, so the
073         * rejection's JSON-RPC error is not rendered on these paths. Header safety
074         * and framework-owned status rules remain checked before transport.
075         *
076         * @param admissionRejection HTTP status and application response headers
077         * @return immutable rejected decision
078         * @throws NullPointerException if the rejection is null
079         */
080        @NonNull
081        static Rejected rejected(@NonNull McpAdmissionRejection admissionRejection) {
082                return new Rejected(admissionRejection);
083        }
084
085        /**
086         * Accepted HTTP identity, explicit expiry and immutable family selection.
087         *
088         * @author <a href="https://www.revetkn.com">Mark Allen</a>
089         */
090        @ThreadSafe
091        public final class Accepted implements McpSessionTransportAdmissionDecision {
092                @NonNull private final McpAdmissionIdentity identity;
093                @NonNull private final Instant validUntil;
094                @NonNull private final Set<@NonNull McpSubscriptionNotificationType> notificationTypes;
095
096                private Accepted(@NonNull McpAdmissionIdentity admissionIdentity,
097                                @NonNull Instant validUntil,
098                                @NonNull Set<@NonNull McpSubscriptionNotificationType> notificationTypes) {
099                        this.identity = requireNonNull(admissionIdentity);
100                        this.validUntil = requireNonNull(validUntil);
101                        EnumSet<McpSubscriptionNotificationType> copiedTypes = EnumSet.noneOf(McpSubscriptionNotificationType.class);
102                        for (McpSubscriptionNotificationType notificationType : requireNonNull(notificationTypes))
103                                copiedTypes.add(requireNonNull(notificationType));
104                        this.notificationTypes = Collections.unmodifiableSet(copiedTypes);
105                }
106
107                /** @return freshly admitted identity */
108                @NonNull public McpAdmissionIdentity getIdentity() { return this.identity; }
109
110                /** @return application authorization expiry */
111                @NonNull public Instant getValidUntil() { return this.validUntil; }
112
113                /** @return immutable selected families in enum declaration order */
114                @NonNull public Set<@NonNull McpSubscriptionNotificationType> getNotificationTypes() {
115                        return this.notificationTypes;
116                }
117
118                /** @return whether every admission property is structurally equal */
119                @Override
120                public boolean equals(@Nullable Object other) {
121                        return this == other || other instanceof Accepted accepted
122                                        && this.identity.equals(accepted.identity)
123                                        && this.validUntil.equals(accepted.validUntil)
124                                        && this.notificationTypes.equals(accepted.notificationTypes);
125                }
126
127                /** @return value-based hash code */
128                @Override public int hashCode() { return Objects.hash(this.identity, this.validUntil, this.notificationTypes); }
129
130                /** @return redacted diagnostic rendering */
131                @Override @NonNull public String toString() {
132                        return "Accepted{identity=<redacted>, validUntil=<redacted>, notificationTypeCount=" + this.notificationTypes.size() + "}";
133                }
134        }
135
136        /**
137         * Rejected HTTP decision retaining the rejection carrier. Soklet validates
138         * its HTTP headers before applying it to the transport.
139         *
140         * @author <a href="https://www.revetkn.com">Mark Allen</a>
141         */
142        @ThreadSafe
143        public final class Rejected implements McpSessionTransportAdmissionDecision {
144                @NonNull private final McpAdmissionRejection rejection;
145
146                private Rejected(@NonNull McpAdmissionRejection admissionRejection) {
147                        this.rejection = requireNonNull(admissionRejection);
148                }
149
150                /** @return HTTP status and response-header rejection carrier */
151                @NonNull public McpAdmissionRejection getRejection() { return this.rejection; }
152
153                /** @return whether this decision carries the same rejection */
154                @Override
155                public boolean equals(@Nullable Object other) {
156                        return this == other || other instanceof Rejected rejected && this.rejection.equals(rejected.rejection);
157                }
158
159                /** @return value-based hash code */
160                @Override public int hashCode() { return this.rejection.hashCode(); }
161
162                /** @return redacted diagnostic rendering */
163                @Override @NonNull public String toString() { return "Rejected{rejection=<redacted>}"; }
164        }
165}