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.util.List;
025import java.util.Objects;
026import java.util.Optional;
027
028import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata;
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Immutable ordered argument suggestions returned by an MCP completer.
033 *
034 * <p>Values remain exact application-provided strings. Soklet does not sort,
035 * normalize, deduplicate, translate, or perform prefix matching on them.
036 * Unpaired UTF-16 surrogates are rejected at construction. The independent
037 * JSON string/token and response-size limits described by {@link McpJsonString}
038 * remain serialization checks.
039 *
040 * @author <a href="https://www.revetkn.com">Mark Allen</a>
041 */
042@ThreadSafe
043public final class McpArgumentCompletionResult implements McpOperationResult {
044        private static final int MAX_VALUES = 100;
045        private static final long MAX_SAFE_TOTAL = 9_007_199_254_740_991L;
046
047        @NonNull
048        private final List<@NonNull String> values;
049        @Nullable
050        private final Long total;
051        @Nullable
052        private final Boolean hasMore;
053        @NonNull
054        private final McpJsonObject metadata;
055
056        /**
057         * Creates a result containing only ordered suggestions.
058         *
059         * @param values zero to 100 exact, non-null suggestion strings
060         * @return immutable result
061         * @throws IllegalArgumentException if there are more than 100 values or a value contains an unpaired surrogate
062         */
063        @NonNull
064        public static McpArgumentCompletionResult fromValues(
065                        @NonNull List<@NonNull String> values) {
066                return withValues(values).build();
067        }
068
069        /**
070         * Begins a result with ordered suggestions and optional fields.
071         *
072         * @param values zero to 100 exact, non-null suggestion strings
073         * @return mutable builder
074         * @throws IllegalArgumentException if there are more than 100 values or a value contains an unpaired surrogate
075         */
076        @NonNull
077        public static Builder withValues(@NonNull List<@NonNull String> values) {
078                return new Builder(values);
079        }
080
081        private McpArgumentCompletionResult(@NonNull Builder builder) {
082                this.values = builder.values;
083                this.total = builder.total;
084                this.hasMore = builder.hasMore;
085                this.metadata = builder.metadata;
086        }
087
088        /** @return immutable suggestions in application order */
089        @NonNull
090        public List<@NonNull String> getValues() {
091                return this.values;
092        }
093
094        /** @return exact match count, or empty when unknown or omitted */
095        @NonNull
096        public Optional<@NonNull Long> getTotal() {
097                return Optional.ofNullable(this.total);
098        }
099
100        /** @return whether additional values exist, or empty when omitted */
101        @NonNull
102        public Optional<@NonNull Boolean> getHasMore() {
103                return Optional.ofNullable(this.hasMore);
104        }
105
106        /** @return result-level protocol extension metadata */
107        @NonNull
108        public McpJsonObject getMetadata() {
109                return this.metadata;
110        }
111
112        /** @return whether values, optional fields, and metadata match */
113        @Override
114        public boolean equals(@Nullable Object other) {
115                if (this == other)
116                        return true;
117                if (!(other instanceof McpArgumentCompletionResult result))
118                        return false;
119                return this.values.equals(result.values)
120                                && Objects.equals(this.total, result.total)
121                                && Objects.equals(this.hasMore, result.hasMore)
122                                && this.metadata.equals(result.metadata);
123        }
124
125        /** @return structural hash code */
126        @Override
127        public int hashCode() {
128                return Objects.hash(this.values, this.total, this.hasMore,
129                                this.metadata);
130        }
131
132        /** @return diagnostic rendering without suggestions or metadata */
133        @Override
134        @NonNull
135        public String toString() {
136                return "McpArgumentCompletionResult{values=<redacted>, total=<redacted>, "
137                                + "hasMore=<redacted>, metadata=<redacted>}";
138        }
139
140        /**
141         * Mutable builder for an immutable argument-completion result.
142         *
143         * @author <a href="https://www.revetkn.com">Mark Allen</a>
144         */
145        @NotThreadSafe
146        public static final class Builder {
147                @NonNull
148                private final List<@NonNull String> values;
149                @Nullable
150                private Long total;
151                @Nullable
152                private Boolean hasMore;
153                @NonNull
154                private McpJsonObject metadata = McpJsonObject.emptyInstance();
155
156                private Builder(@NonNull List<@NonNull String> values) {
157                        this.values = List.copyOf(requireNonNull(values));
158                        if (this.values.size() > MAX_VALUES)
159                                throw new IllegalArgumentException(
160                                                "MCP completion results must not exceed 100 values.");
161                        this.values.forEach(McpContentValueSupport::requireWellFormedString);
162                }
163
164                /**
165                 * Sets the exact count of matching values.
166                 *
167                 * @param total count in the JavaScript-safe integer range
168                 * @return this builder
169                 */
170                @NonNull
171                public Builder total(@NonNull Long total) {
172                        this.total = requireNonNull(total);
173                        return this;
174                }
175
176                /**
177                 * Sets whether additional values exist.
178                 *
179                 * @param hasMore true when results were truncated
180                 * @return this builder
181                 */
182                @NonNull
183                public Builder hasMore(@NonNull Boolean hasMore) {
184                        this.hasMore = requireNonNull(hasMore);
185                        return this;
186                }
187
188                /**
189                 * Sets result-level protocol extension metadata.
190                 *
191                 * @param metadata application metadata without MCP-reserved prefixes
192                 * @return this builder
193                 */
194                @NonNull
195                public Builder metadata(@NonNull McpJsonObject metadata) {
196                        this.metadata = requireApplicationMetadata(metadata);
197                        return this;
198                }
199
200                /**
201                 * Validates cross-field consistency and builds an immutable result.
202                 *
203                 * @return immutable result
204                 */
205                @NonNull
206                public McpArgumentCompletionResult build() {
207                        if (this.total != null) {
208                                if (this.total < this.values.size()
209                                                || this.total > MAX_SAFE_TOTAL)
210                                        throw new IllegalArgumentException(
211                                                        "MCP completion total is out of range.");
212                                if (this.hasMore != null
213                                                && this.hasMore != (this.total > this.values.size()))
214                                        throw new IllegalArgumentException(
215                                                        "MCP completion total and hasMore disagree.");
216                        }
217                        return new McpArgumentCompletionResult(this);
218                }
219        }
220}