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.Objects;
025import java.util.Optional;
026
027import static java.util.Objects.requireNonNull;
028
029/**
030 * Immutable progress update for one active MCP invocation.
031 *
032 * <p>Progress and total values are finite JSON numbers. Soklet associates the
033 * update with the initiating request's opaque progress token; applications do
034 * not copy or manage that token themselves.
035 *
036 * <p>String factories and setters reject unpaired UTF-16 surrogates with
037 * {@link IllegalArgumentException}; well-formed Unicode is preserved exactly.
038 *
039 * @author <a href="https://www.revetkn.com">Mark Allen</a>
040 */
041@ThreadSafe
042public final class McpProgressUpdate {
043        private final double progress;
044        private final @Nullable Double total;
045        private final @Nullable String message;
046
047        /**
048         * Vends a builder primed with the current progress value.
049         *
050         * @param progress finite progress value
051         * @return progress-update builder
052         * @throws NullPointerException if {@code progress} is null
053         * @throws IllegalArgumentException if {@code progress} is not finite
054         */
055        @NonNull
056        public static Builder withProgress(@NonNull Double progress) {
057                return new Builder(requireNonNull(progress));
058        }
059
060        private McpProgressUpdate(@NonNull Builder builder) {
061                this.progress = requireFinite(builder.progress, "Progress");
062                this.total = builder.total == null ? null
063                                : requireFinite(builder.total, "Progress total");
064                this.message = builder.message;
065        }
066
067        /** @return finite progress value */
068        @NonNull
069        public Double getProgress() {
070                return this.progress;
071        }
072
073        /** @return finite total value, if supplied */
074        @NonNull
075        public Optional<@NonNull Double> getTotal() {
076                return Optional.ofNullable(this.total);
077        }
078
079        /** @return human-readable progress message, if supplied */
080        @NonNull
081        public Optional<@NonNull String> getMessage() {
082                return Optional.ofNullable(this.message);
083        }
084
085        /** @return whether every progress property is structurally equal */
086        @Override
087        public boolean equals(@Nullable Object other) {
088                if (this == other)
089                        return true;
090                if (!(other instanceof McpProgressUpdate update))
091                        return false;
092                return Double.compare(this.progress, update.progress) == 0
093                                && Objects.equals(this.total, update.total)
094                                && Objects.equals(this.message, update.message);
095        }
096
097        /** @return structural progress-update hash code */
098        @Override
099        public int hashCode() {
100                return Objects.hash(this.progress, this.total, this.message);
101        }
102
103        private static double requireFinite(double value, @NonNull String description) {
104                if (!Double.isFinite(value))
105                        throw new IllegalArgumentException(description + " must be finite.");
106                // JSON has one zero value. Normalize the Java-only negative-zero spelling.
107                return value == 0.0d ? 0.0d : value;
108        }
109
110        /**
111         * Mutable builder for immutable {@link McpProgressUpdate} values.
112         *
113         * @author <a href="https://www.revetkn.com">Mark Allen</a>
114         */
115        @NotThreadSafe
116        public static final class Builder {
117                private final double progress;
118                private @Nullable Double total;
119                private @Nullable String message;
120
121                private Builder(double progress) {
122                        this.progress = requireFinite(progress, "Progress");
123                }
124
125                /**
126                 * Supplies the total amount of work, when known.
127                 *
128                 * @param total finite total value
129                 * @return this builder
130                 * @throws NullPointerException if {@code total} is null
131                 * @throws IllegalArgumentException if {@code total} is not finite
132                 */
133                @NonNull
134                public Builder total(@NonNull Double total) {
135                        this.total = requireFinite(requireNonNull(total), "Progress total");
136                        return this;
137                }
138
139                /**
140                 * Supplies an optional human-readable progress message.
141                 *
142                 * @param message progress message
143                 * @return this builder
144                 * @throws NullPointerException if {@code message} is null
145                 * @throws IllegalArgumentException if {@code message} contains unpaired UTF-16 surrogates
146                 */
147                @NonNull
148                public Builder message(@NonNull String message) {
149                        this.message = McpContentValueSupport.requireWellFormedString(message);
150                        return this;
151                }
152
153                /** @return immutable progress update */
154                @NonNull
155                public McpProgressUpdate build() {
156                        return new McpProgressUpdate(this);
157                }
158        }
159}