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