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}