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.Optional;
025
026import static com.soklet.McpContentValueSupport.annotationsEqual;
027import static com.soklet.McpContentValueSupport.annotationsHashCode;
028import static com.soklet.internal.mcp.protocol.McpApplicationMetadata.requireApplicationMetadata;
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Immutable textual MCP content block.
033 *
034 * <p>Construction rejects unpaired UTF-16 surrogates with
035 * {@link IllegalArgumentException}. Well-formed text is retained exactly,
036 * without normalization or replacement.</p>
037 *
038 * <p>Text must fit the independent decoded-string and escaped-token JSON
039 * limits described by {@link McpJsonString}, as well as the complete response
040 * byte limit. Creating this content block does not prevalidate its eventual
041 * serialized response.</p>
042 *
043 * @author <a href="https://www.revetkn.com">Mark Allen</a>
044 */
045@ThreadSafe
046public final class McpTextContent implements McpContentBlock {
047        @NonNull
048        private final String text;
049        @Nullable
050        private final McpContentAnnotations annotations;
051        @NonNull
052        private final McpJsonObject metadata;
053
054        /**
055         * Creates unannotated text content.
056         *
057         * @param text text value
058         * @return immutable text content
059         * @throws IllegalArgumentException if the text contains an unpaired surrogate
060         */
061        @NonNull
062        public static McpTextContent fromText(@NonNull String text) {
063                return withText(text).build();
064        }
065
066        /**
067         * Vends a builder primed with the text value.
068         *
069         * @param text text value
070         * @return text-content builder
071         * @throws IllegalArgumentException if the text contains an unpaired surrogate
072         */
073        @NonNull
074        public static Builder withText(@NonNull String text) {
075                return new Builder(text);
076        }
077
078        private McpTextContent(@NonNull Builder builder) {
079                this.text = builder.text;
080                this.annotations = builder.annotations;
081                this.metadata = requireApplicationMetadata(builder.metadata);
082        }
083
084        /** @return text value */
085        @NonNull
086        public String getText() {
087                return this.text;
088        }
089
090        /** @return content annotations, if supplied */
091        @Override
092        @NonNull
093        public Optional<@NonNull McpContentAnnotations> getAnnotations() {
094                return Optional.ofNullable(this.annotations);
095        }
096
097        /** @return immutable extension metadata */
098        @Override
099        @NonNull
100        public McpJsonObject getMetadata() {
101                return this.metadata;
102        }
103
104        /** @return whether every content property is structurally equal */
105        @Override
106        public boolean equals(@Nullable Object other) {
107                if (this == other)
108                        return true;
109                if (!(other instanceof McpTextContent content))
110                        return false;
111                return this.text.equals(content.text)
112                                && annotationsEqual(this.annotations, content.annotations)
113                                && this.metadata.equals(content.metadata);
114        }
115
116        /** @return structural content hash code */
117        @Override
118        public int hashCode() {
119                int result = this.text.hashCode();
120                result = 31 * result + annotationsHashCode(this.annotations);
121                result = 31 * result + this.metadata.hashCode();
122                return result;
123        }
124
125        /**
126         * Mutable builder for immutable {@link McpTextContent}.
127         *
128         * @author <a href="https://www.revetkn.com">Mark Allen</a>
129         */
130        @NotThreadSafe
131        public static final class Builder {
132                @NonNull
133                private final String text;
134                @Nullable
135                private McpContentAnnotations annotations;
136                @NonNull
137                private McpJsonObject metadata = McpJsonObject.emptyInstance();
138
139                private Builder(@NonNull String text) {
140                        this.text = McpContentValueSupport.requireWellFormedString(text);
141                }
142
143                /** @param annotations content annotations
144                 * @return this builder */
145                @NonNull
146                public Builder annotations(@NonNull McpContentAnnotations annotations) {
147                        this.annotations = requireNonNull(annotations);
148                        return this;
149                }
150
151                /** @param metadata protocol extension metadata
152                 * @return this builder */
153                @NonNull
154                public Builder metadata(@NonNull McpJsonObject metadata) {
155                        this.metadata = requireNonNull(metadata);
156                        return this;
157                }
158
159                /** @return immutable text content */
160                @NonNull
161                public McpTextContent build() {
162                        return new McpTextContent(this);
163                }
164        }
165}