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.ArrayList; 025import java.util.List; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.stream.Collectors; 029 030import static java.lang.String.format; 031import static java.util.Objects.requireNonNull; 032 033/** 034 * Encapsulates the results of a request that would normally be handled by your {@link Server} (both logical response and bytes to be sent over the wire), used for integration testing via {@link Simulator#performRequest(Request)}. 035 * <p> 036 * Instances can be acquired via the {@link #withMarshaledResponse(MarshaledResponse)} builder factory method. 037 * A convenience instance factory is also available via {@link #fromMarshaledResponse(MarshaledResponse)}. 038 * <p> 039 * The Server-Sent Event equivalent of this type is {@link ServerSentEventRequestResult}, which is used for integration testing via {@link Simulator#performServerSentEventRequest(Request)}. 040 * <p> 041 * See <a href="https://www.soklet.com/docs/testing#integration-testing">https://www.soklet.com/docs/testing#integration-testing</a> for detailed documentation. 042 * 043 * @author <a href="https://www.revetkn.com">Mark Allen</a> 044 */ 045@ThreadSafe 046public final class RequestResult { 047 @NonNull 048 private final MarshaledResponse marshaledResponse; 049 @Nullable 050 private final Response response; 051 @Nullable 052 private final CorsPreflightResponse corsPreflightResponse; 053 @Nullable 054 private final ResourceMethod resourceMethod; 055 @Nullable 056 private final HandshakeResult handshakeResult; 057 058 /** 059 * Acquires a builder for {@link RequestResult} instances. 060 * 061 * @param marshaledResponse the bytes that will ultimately be written over the wire 062 * @return the builder 063 */ 064 @NonNull 065 public static Builder withMarshaledResponse(@NonNull MarshaledResponse marshaledResponse) { 066 requireNonNull(marshaledResponse); 067 return new Builder(marshaledResponse); 068 } 069 070 /** 071 * Creates a {@link RequestResult} from a marshaled response without additional customization. 072 * 073 * @param marshaledResponse the bytes that will ultimately be written over the wire 074 * @return a {@link RequestResult} instance 075 */ 076 @NonNull 077 public static RequestResult fromMarshaledResponse(@NonNull MarshaledResponse marshaledResponse) { 078 return withMarshaledResponse(marshaledResponse).build(); 079 } 080 081 /** 082 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 083 * 084 * @return a copier for this instance 085 */ 086 @NonNull 087 public Copier copy() { 088 return new Copier(this); 089 } 090 091 protected RequestResult(@NonNull Builder builder) { 092 requireNonNull(builder); 093 094 this.marshaledResponse = builder.marshaledResponse; 095 this.response = builder.response; 096 this.corsPreflightResponse = builder.corsPreflightResponse; 097 this.resourceMethod = builder.resourceMethod; 098 this.handshakeResult = builder.handshakeResult; 099 } 100 101 @Override 102 public String toString() { 103 List<String> components = new ArrayList<>(5); 104 105 components.add(format("marshaledResponse=%s", getMarshaledResponse())); 106 107 Response response = getResponse().orElse(null); 108 109 if (response != null) 110 components.add(format("response=%s", response)); 111 112 CorsPreflightResponse corsPreflightResponse = getCorsPreflightResponse().orElse(null); 113 114 if (corsPreflightResponse != null) 115 components.add(format("corsPreflightResponse=%s", corsPreflightResponse)); 116 117 ResourceMethod resourceMethod = getResourceMethod().orElse(null); 118 119 if (resourceMethod != null) 120 components.add(format("resourceMethod=%s", resourceMethod)); 121 122 // Hide this for now because handshake info is package-private and we don't want it to leak out 123 124 // HandshakeResult handshakeResult = getHandshakeResult().orElse(null); 125 126 // if (handshakeResult != null) 127 // components.add(format("handshakeResult=%s", handshakeResult)); 128 129 return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", "))); 130 } 131 132 @Override 133 public boolean equals(@Nullable Object object) { 134 if (this == object) 135 return true; 136 137 if (!(object instanceof RequestResult requestResult)) 138 return false; 139 140 return Objects.equals(getMarshaledResponse(), requestResult.getMarshaledResponse()) 141 && Objects.equals(getResponse(), requestResult.getResponse()) 142 && Objects.equals(getCorsPreflightResponse(), requestResult.getCorsPreflightResponse()) 143 && Objects.equals(getResourceMethod(), requestResult.getResourceMethod()) 144 && Objects.equals(getHandshakeResult(), requestResult.getHandshakeResult()); 145 } 146 147 @Override 148 public int hashCode() { 149 return Objects.hash(getMarshaledResponse(), getResponse(), getCorsPreflightResponse(), getResourceMethod(), getHandshakeResult()); 150 } 151 152 /** 153 * The final representation of the response to be written over the wire. 154 * 155 * @return the response to be written over the wire 156 */ 157 @NonNull 158 public MarshaledResponse getMarshaledResponse() { 159 return this.marshaledResponse; 160 } 161 162 /** 163 * The logical response, determined by the return value of the <em>Resource Method</em> (if available). 164 * 165 * @return the logical response 166 */ 167 @NonNull 168 public Optional<Response> getResponse() { 169 return Optional.ofNullable(this.response); 170 } 171 172 /** 173 * The CORS preflight logical response, if applicable for the request. 174 * 175 * @return the CORS preflight logical response 176 */ 177 @NonNull 178 public Optional<CorsPreflightResponse> getCorsPreflightResponse() { 179 return Optional.ofNullable(this.corsPreflightResponse); 180 } 181 182 /** 183 * The <em>Resource Method</em> that handled the request, if available. 184 * 185 * @return the <em>Resource Method</em> that handled the request 186 */ 187 @NonNull 188 public Optional<ResourceMethod> getResourceMethod() { 189 return Optional.ofNullable(this.resourceMethod); 190 } 191 192 193 /** 194 * The SSE handshake result, if available. 195 * 196 * @return the SSE handshake result 197 */ 198 @NonNull 199 Optional<HandshakeResult> getHandshakeResult() { 200 return Optional.ofNullable(this.handshakeResult); 201 } 202 203 /** 204 * Builder used to construct instances of {@link RequestResult} via {@link RequestResult#withMarshaledResponse(MarshaledResponse)}. 205 * <p> 206 * This class is intended for use by a single thread. 207 * 208 * @author <a href="https://www.revetkn.com">Mark Allen</a> 209 */ 210 @NotThreadSafe 211 public static final class Builder { 212 @NonNull 213 private MarshaledResponse marshaledResponse; 214 @Nullable 215 private Response response; 216 @Nullable 217 private CorsPreflightResponse corsPreflightResponse; 218 @Nullable 219 private ResourceMethod resourceMethod; 220 @Nullable 221 private HandshakeResult handshakeResult; 222 223 protected Builder(@NonNull MarshaledResponse marshaledResponse) { 224 requireNonNull(marshaledResponse); 225 this.marshaledResponse = marshaledResponse; 226 } 227 228 @NonNull 229 public Builder marshaledResponse(@NonNull MarshaledResponse marshaledResponse) { 230 requireNonNull(marshaledResponse); 231 this.marshaledResponse = marshaledResponse; 232 return this; 233 } 234 235 @NonNull 236 public Builder response(@Nullable Response response) { 237 this.response = response; 238 return this; 239 } 240 241 @NonNull 242 public Builder corsPreflightResponse(@Nullable CorsPreflightResponse corsPreflightResponse) { 243 this.corsPreflightResponse = corsPreflightResponse; 244 return this; 245 } 246 247 @NonNull 248 public Builder resourceMethod(@Nullable ResourceMethod resourceMethod) { 249 this.resourceMethod = resourceMethod; 250 return this; 251 } 252 253 @NonNull 254 Builder handshakeResult(@Nullable HandshakeResult handshakeResult) { 255 this.handshakeResult = handshakeResult; 256 return this; 257 } 258 259 @NonNull 260 public RequestResult build() { 261 return new RequestResult(this); 262 } 263 } 264 265 /** 266 * Builder used to copy instances of {@link RequestResult} via {@link RequestResult#copy()}. 267 * <p> 268 * This class is intended for use by a single thread. 269 * 270 * @author <a href="https://www.revetkn.com">Mark Allen</a> 271 */ 272 @NotThreadSafe 273 public static final class Copier { 274 @NonNull 275 private final Builder builder; 276 277 Copier(@NonNull RequestResult requestResult) { 278 requireNonNull(requestResult); 279 280 this.builder = new Builder(requestResult.getMarshaledResponse()) 281 .response(requestResult.getResponse().orElse(null)) 282 .corsPreflightResponse(requestResult.getCorsPreflightResponse().orElse(null)) 283 .resourceMethod(requestResult.getResourceMethod().orElse(null)) 284 .handshakeResult(requestResult.getHandshakeResult().orElse(null)); 285 } 286 287 @NonNull 288 public Copier marshaledResponse(@NonNull MarshaledResponse marshaledResponse) { 289 requireNonNull(marshaledResponse); 290 this.builder.marshaledResponse(marshaledResponse); 291 return this; 292 } 293 294 @NonNull 295 public Copier response(@Nullable Response response) { 296 this.builder.response(response); 297 return this; 298 } 299 300 @NonNull 301 public Copier corsPreflightResponse(@Nullable CorsPreflightResponse corsPreflightResponse) { 302 this.builder.corsPreflightResponse(corsPreflightResponse); 303 return this; 304 } 305 306 @NonNull 307 public Copier resourceMethod(@Nullable ResourceMethod resourceMethod) { 308 this.builder.resourceMethod(resourceMethod); 309 return this; 310 } 311 312 @NonNull 313 Copier handshakeResult(@Nullable HandshakeResult handshakeResult) { 314 this.builder.handshakeResult(handshakeResult); 315 return this; 316 } 317 318 @NonNull 319 public RequestResult finish() { 320 return this.builder.build(); 321 } 322 } 323}