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.Collections;
026import java.util.LinkedHashSet;
027import java.util.Objects;
028import java.util.Optional;
029import java.util.Set;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Immutable optional annotations for an MCP content block.
035 *
036 * <p>The annotation vocabulary follows the selected MCP revision and may gain
037 * fields in a future supported profile. Applications should ignore
038 * annotation data they do not use rather than treating today's fields as a
039 * permanent closed set.
040 *
041 * @author <a href="https://www.revetkn.com">Mark Allen</a>
042 */
043@ThreadSafe
044public final class McpContentAnnotations {
045        @NonNull
046        private final Set<@NonNull McpRole> audience;
047        @Nullable
048        private final Double priority;
049        @Nullable
050        private final Instant lastModified;
051
052        /** @return an empty content-annotation builder */
053        @NonNull
054        public static Builder builder() {
055                return new Builder();
056        }
057
058        private McpContentAnnotations(@NonNull Builder builder) {
059                this.audience = Collections.unmodifiableSet(
060                                new LinkedHashSet<>(builder.audience));
061                this.priority = builder.priority;
062                this.lastModified = builder.lastModified;
063        }
064
065        /** @return immutable intended audience */
066        @NonNull
067        public Set<@NonNull McpRole> getAudience() {
068                return this.audience;
069        }
070
071        /** @return priority in the inclusive range {@code 0.0..1.0}, if set */
072        @NonNull
073        public Optional<@NonNull Double> getPriority() {
074                return Optional.ofNullable(this.priority);
075        }
076
077        /** @return last-modified instant, if set */
078        @NonNull
079        public Optional<@NonNull Instant> getLastModified() {
080                return Optional.ofNullable(this.lastModified);
081        }
082
083        /** @return whether every annotation property is structurally equal */
084        @Override
085        public boolean equals(@Nullable Object other) {
086                if (this == other)
087                        return true;
088                if (!(other instanceof McpContentAnnotations annotations))
089                        return false;
090                return this.audience.equals(annotations.audience)
091                                && Objects.equals(this.priority, annotations.priority)
092                                && Objects.equals(this.lastModified, annotations.lastModified);
093        }
094
095        /** @return structural annotation hash code */
096        @Override
097        public int hashCode() {
098                return Objects.hash(this.audience, this.priority, this.lastModified);
099        }
100
101        /**
102         * Mutable builder for immutable {@link McpContentAnnotations}.
103         *
104         * @author <a href="https://www.revetkn.com">Mark Allen</a>
105         */
106        @NotThreadSafe
107        public static final class Builder {
108                @NonNull
109                private Set<@NonNull McpRole> audience = new LinkedHashSet<>();
110                @Nullable
111                private Double priority;
112                @Nullable
113                private Instant lastModified;
114
115                private Builder() {
116                }
117
118                /**
119                 * Replaces the intended audience.
120                 * Passing no roles clears the intended audience.
121                 *
122                 * @param audience audience roles
123                 * @return this builder
124                 * @throws NullPointerException if the array or any role is null
125                 */
126                @NonNull
127                public Builder audience(
128                                @NonNull McpRole @NonNull... audience) {
129                        requireNonNull(audience);
130                        Set<McpRole> copied = new LinkedHashSet<>();
131                        for (McpRole role : audience)
132                                copied.add(requireNonNull(role));
133                        this.audience = copied;
134                        return this;
135                }
136
137                /**
138                 * Sets a content priority.
139                 *
140                 * @param priority priority in the inclusive range {@code 0.0..1.0}
141                 * @return this builder
142                 * @throws NullPointerException if {@code priority} is null
143                 * @throws IllegalArgumentException if the value is non-finite or outside
144                 * the permitted range
145                 */
146                @NonNull
147                public Builder priority(@NonNull Double priority) {
148                        requireNonNull(priority);
149                        if (!Double.isFinite(priority) || priority < 0.0 || priority > 1.0)
150                                throw new IllegalArgumentException(
151                                                "Content priority must be finite and between 0.0 and 1.0.");
152                        this.priority = priority;
153                        return this;
154                }
155
156                /**
157                 * Sets the last-modified time.
158                 *
159                 * @param lastModified last-modified time
160                 * @return this builder
161                 */
162                @NonNull
163                public Builder lastModified(@NonNull Instant lastModified) {
164                        this.lastModified = requireNonNull(lastModified);
165                        return this;
166                }
167
168                /** @return immutable content annotations */
169                @NonNull
170                public McpContentAnnotations build() {
171                        return new McpContentAnnotations(this);
172                }
173        }
174}