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.math.BigDecimal;
025import java.util.Collections;
026import java.util.LinkedHashMap;
027import java.util.Map;
028import java.util.Optional;
029
030import static java.util.Objects.requireNonNull;
031
032/**
033 * An immutable JSON object.
034 *
035 * <p>Member names are retained exactly, without normalization or replacement.
036 * Factories and builder setters reject names containing unpaired UTF-16
037 * surrogates with {@link IllegalArgumentException}. The independent string/token
038 * and response-size limits described by {@link McpJsonString} are checked when
039 * the object is serialized.</p>
040 *
041 * @author <a href="https://www.revetkn.com">Mark Allen</a>
042 */
043@ThreadSafe
044public final class McpJsonObject implements McpJsonValue {
045        @NonNull
046        private static final McpJsonObject EMPTY = new McpJsonObject(Map.of());
047        @NonNull
048        private final Map<@NonNull String, @NonNull McpJsonValue> members;
049
050        /**
051         * Returns the shared empty object.
052         *
053         * @return empty JSON object
054         */
055        @NonNull
056        public static McpJsonObject emptyInstance() {
057                return EMPTY;
058        }
059
060        /**
061         * Vends a mutable builder.
062         *
063         * @return JSON object builder
064         */
065        @NonNull
066        public static Builder builder() {
067                return new Builder();
068        }
069
070        /**
071         * Creates an object from the supplied members.
072         *
073         * @param members object members
074         * @return immutable JSON object
075         * @throws IllegalArgumentException if a member name contains an unpaired surrogate
076         */
077        @NonNull
078        public static McpJsonObject fromMembers(
079                        @NonNull Map<@NonNull String, ? extends @NonNull McpJsonValue> members) {
080                requireNonNull(members);
081                if (members.isEmpty())
082                        return emptyInstance();
083                return new McpJsonObject(members);
084        }
085
086        private McpJsonObject(
087                        @NonNull Map<@NonNull String, ? extends @NonNull McpJsonValue> members) {
088                LinkedHashMap<@NonNull String, @NonNull McpJsonValue> copied =
089                                new LinkedHashMap<>();
090                members.forEach((name, value) -> copied.put(
091                                McpContentValueSupport.requireWellFormedString(name), requireNonNull(value)));
092                this.members = Collections.unmodifiableMap(copied);
093        }
094
095        /**
096         * Returns the immutable members in their original insertion order.
097         *
098         * @return object members in insertion order
099         */
100        @NonNull
101        public Map<@NonNull String, @NonNull McpJsonValue> getMembers() {
102                return this.members;
103        }
104
105        /**
106         * Looks up a member by name.
107         *
108         * @param name member name
109         * @return member value, if present
110         */
111        @NonNull
112        public Optional<@NonNull McpJsonValue> find(@NonNull String name) {
113                return Optional.ofNullable(this.members.get(requireNonNull(name)));
114        }
115
116        /** @return whether this object has structurally equal members */
117        @Override
118        public boolean equals(@Nullable Object other) {
119                if (this == other)
120                        return true;
121                if (!(other instanceof McpJsonObject object))
122                        return false;
123                return this.members.equals(object.members);
124        }
125
126        /** @return structural member hash code */
127        @Override
128        public int hashCode() {
129                return this.members.hashCode();
130        }
131
132        /**
133         * Mutable builder for {@link McpJsonObject}.
134         *
135         * @author <a href="https://www.revetkn.com">Mark Allen</a>
136         */
137        @NotThreadSafe
138        public static final class Builder {
139                @NonNull
140                private final Map<@NonNull String, @NonNull McpJsonValue> members =
141                                new LinkedHashMap<>();
142
143                private Builder() {
144                }
145
146                /**
147                 * Associates a member name with a JSON value.
148                 *
149                 * @param name  the member name
150                 * @param value the member value
151                 * @return this builder
152                 * @throws IllegalArgumentException if the member name contains an unpaired surrogate
153                 */
154                @NonNull
155                public Builder put(@NonNull String name, @NonNull McpJsonValue value) {
156                        this.members.put(McpContentValueSupport.requireWellFormedString(name), requireNonNull(value));
157                        return this;
158                }
159
160                /**
161                 * Associates a member name with a JSON string.
162                 *
163                 * @param name  the member name
164                 * @param value the string value
165                 * @return this builder
166                 * @throws IllegalArgumentException if the name or value contains an unpaired surrogate
167                 */
168                @NonNull
169                public Builder put(@NonNull String name, @NonNull String value) {
170                        return put(name, McpJsonString.fromValue(value));
171                }
172
173                /**
174                 * Associates a member name with an exactly represented JSON number.
175                 *
176                 * @param name  the member name
177                 * @param value the number value
178                 * @return this builder
179                 */
180                @NonNull
181                public Builder put(@NonNull String name, @NonNull BigDecimal value) {
182                        return put(name, McpJsonNumber.fromValue(value));
183                }
184
185                /**
186                 * Associates a member name with an integral JSON number.
187                 *
188                 * @param name  the member name
189                 * @param value the integer value
190                 * @return this builder
191                 * @throws NullPointerException if {@code value} is null
192                 */
193                @NonNull
194                public Builder put(@NonNull String name, @NonNull Integer value) {
195                        return put(name, BigDecimal.valueOf(requireNonNull(value)));
196                }
197
198                /**
199                 * Associates a member name with an integral JSON number.
200                 *
201                 * @param name  the member name
202                 * @param value the long value
203                 * @return this builder
204                 * @throws NullPointerException if {@code value} is null
205                 */
206                @NonNull
207                public Builder put(@NonNull String name, @NonNull Long value) {
208                        return put(name, BigDecimal.valueOf(requireNonNull(value)));
209                }
210
211                /**
212                 * Associates a member name with a finite JSON number.
213                 *
214                 * @param name  the member name
215                 * @param value the finite double value
216                 * @return this builder
217                 * @throws NullPointerException if {@code value} is null
218                 * @throws IllegalArgumentException if {@code value} is not finite
219                 */
220                @NonNull
221                public Builder put(@NonNull String name, @NonNull Double value) {
222                        requireNonNull(value);
223                        if (!Double.isFinite(value))
224                                throw new IllegalArgumentException("JSON numbers must be finite.");
225                        return put(name, BigDecimal.valueOf(value));
226                }
227
228                /**
229                 * Associates a member name with a JSON boolean.
230                 *
231                 * @param name  the member name
232                 * @param value the boolean value
233                 * @return this builder
234                 * @throws NullPointerException if {@code value} is null
235                 */
236                @NonNull
237                public Builder put(@NonNull String name, @NonNull Boolean value) {
238                        return put(name, McpJsonBoolean.fromValue(requireNonNull(value)));
239                }
240
241                /**
242                 * Associates a member name with the JSON {@code null} value.
243                 *
244                 * @param name the member name
245                 * @return this builder
246                 */
247                @NonNull
248                public Builder putNull(@NonNull String name) {
249                        return put(name, McpJsonNull.INSTANCE);
250                }
251
252                /**
253                 * Builds an immutable JSON object.
254                 *
255                 * @return the JSON object
256                 */
257                @NonNull
258                public McpJsonObject build() {
259                        return McpJsonObject.fromMembers(this.members);
260                }
261        }
262}