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;
020
021import javax.annotation.concurrent.NotThreadSafe;
022import javax.annotation.concurrent.ThreadSafe;
023import java.util.Collections;
024import java.util.EnumSet;
025import java.util.Set;
026
027import static java.util.Objects.requireNonNull;
028
029/**
030 * Immutable endpoint-scoped MCP subscription configuration.
031 * <p>
032 * The configured notification types are the subscription-change families the
033 * endpoint may support. The publisher is application-owned and may provide
034 * either process-local or distributed broadcast delivery. This value does not
035 * transfer publisher lifecycle ownership to Soklet.
036 *
037 * <p>Instances intentionally retain reference identity because the publisher
038 * is a live application-owned capability, not immutable value data. Code that
039 * compares configurations should compare notification types and publisher
040 * identity according to its own lifecycle model.
041 *
042 * @author <a href="https://www.revetkn.com">Mark Allen</a>
043 */
044@ThreadSafe
045public final class McpSubscriptionConfig {
046        @NonNull
047        private final McpSubscriptionEventPublisher eventPublisher;
048        @NonNull
049        private final Set<@NonNull McpSubscriptionNotificationType> notificationTypes;
050
051        /**
052         * Vends a builder primed with its required construction values.
053         *
054         * @param subscriptionEventPublisher application-owned broadcast publisher
055         * @param subscriptionNotificationTypes nonempty supported notification families
056         * @return a subscription-configuration builder
057         * @throws NullPointerException if an argument or notification type is null
058         * @throws IllegalArgumentException if {@code subscriptionNotificationTypes} is empty
059         */
060        @NonNull
061        public static Builder withEventPublisherAndNotificationTypes(
062                        @NonNull McpSubscriptionEventPublisher subscriptionEventPublisher,
063                        @NonNull Set<@NonNull McpSubscriptionNotificationType>
064                                        subscriptionNotificationTypes) {
065                return new Builder(subscriptionEventPublisher, subscriptionNotificationTypes);
066        }
067
068        private McpSubscriptionConfig(@NonNull Builder builder) {
069                requireNonNull(builder);
070                this.eventPublisher = builder.eventPublisher;
071                this.notificationTypes = requireNotificationTypes(builder.notificationTypes);
072        }
073
074        @NonNull
075        private static Set<@NonNull McpSubscriptionNotificationType> requireNotificationTypes(
076                        @NonNull Set<@NonNull McpSubscriptionNotificationType>
077                                        subscriptionNotificationTypes) {
078                EnumSet<McpSubscriptionNotificationType> copiedTypes = EnumSet.noneOf(
079                                McpSubscriptionNotificationType.class);
080                for (McpSubscriptionNotificationType subscriptionNotificationType
081                                : requireNonNull(subscriptionNotificationTypes))
082                        copiedTypes.add(requireNonNull(subscriptionNotificationType));
083                if (copiedTypes.isEmpty())
084                        throw new IllegalArgumentException(
085                                        "At least one MCP subscription notification type must be configured.");
086                return Collections.unmodifiableSet(copiedTypes);
087        }
088
089        /**
090         * Returns the application-owned broadcast publisher.
091         *
092         * @return event publisher
093         */
094        @NonNull
095        public McpSubscriptionEventPublisher getEventPublisher() {
096                return this.eventPublisher;
097        }
098
099        /**
100         * Returns the endpoint's supported subscription-notification families.
101         *
102         * @return immutable nonempty notification-type set in enum declaration order
103         */
104        @NonNull
105        public Set<@NonNull McpSubscriptionNotificationType> getNotificationTypes() {
106                return this.notificationTypes;
107        }
108
109        /**
110         * Builder for immutable {@link McpSubscriptionConfig} values.
111         * <p>
112         * This class is intended for use by a single thread.
113         *
114         * @author <a href="https://www.revetkn.com">Mark Allen</a>
115         */
116        @NotThreadSafe
117        public static final class Builder {
118                @NonNull
119                private final McpSubscriptionEventPublisher eventPublisher;
120                @NonNull
121                private Set<@NonNull McpSubscriptionNotificationType>
122                                notificationTypes;
123
124                private Builder(
125                                @NonNull McpSubscriptionEventPublisher subscriptionEventPublisher,
126                                @NonNull Set<@NonNull McpSubscriptionNotificationType>
127                                                subscriptionNotificationTypes) {
128                        this.eventPublisher = requireNonNull(subscriptionEventPublisher);
129                        this.notificationTypes = requireNotificationTypes(subscriptionNotificationTypes);
130                }
131
132                /**
133                 * Replaces the supported subscription-notification families.
134                 * A failed replacement leaves the previous families unchanged.
135                 *
136                 * @param subscriptionNotificationTypes nonempty notification families
137                 * @return this builder
138                 * @throws NullPointerException if the set or any item is null
139                 * @throws IllegalArgumentException if the set is empty
140                 */
141                @NonNull
142                public Builder notificationTypes(
143                                @NonNull Set<@NonNull McpSubscriptionNotificationType>
144                                                subscriptionNotificationTypes) {
145                        this.notificationTypes = requireNotificationTypes(subscriptionNotificationTypes);
146                        return this;
147                }
148
149                /**
150                 * Builds an immutable endpoint subscription configuration.
151                 *
152                 * @return subscription configuration
153                 * @throws IllegalStateException if no notification type was configured
154                 */
155                @NonNull
156                public McpSubscriptionConfig build() {
157                        if (this.notificationTypes.isEmpty())
158                                throw new IllegalStateException(
159                                                "At least one MCP subscription notification type must be configured.");
160                        return new McpSubscriptionConfig(this);
161                }
162        }
163}