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}