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.core;
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 (both logical response and bytes to be sent over the wire),
034 * useful for integration testing via {@link Simulator#performRequest(Request)}.
035 *
036 * @author <a href="https://www.revetkn.com">Mark Allen</a>
037 */
038@ThreadSafe
039public class RequestResult {
040        @Nonnull
041        private final MarshaledResponse marshaledResponse;
042        @Nullable
043        private final Response response;
044        @Nullable
045        private final CorsPreflightResponse corsPreflightResponse;
046        @Nullable
047        private final ResourceMethod resourceMethod;
048
049        /**
050         * Acquires a builder for {@link RequestResult} instances.
051         *
052         * @param marshaledResponse the bytes that will ultimately be written over the wire
053         * @return the builder
054         */
055        @Nonnull
056        public static Builder withMarshaledResponse(@Nonnull MarshaledResponse marshaledResponse) {
057                requireNonNull(marshaledResponse);
058                return new Builder(marshaledResponse);
059        }
060
061        /**
062         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
063         *
064         * @return a copier for this instance
065         */
066        @Nonnull
067        public Copier copy() {
068                return new Copier(this);
069        }
070
071        protected RequestResult(@Nonnull Builder builder) {
072                requireNonNull(builder);
073
074                this.marshaledResponse = builder.marshaledResponse;
075                this.response = builder.response;
076                this.corsPreflightResponse = builder.corsPreflightResponse;
077                this.resourceMethod = builder.resourceMethod;
078        }
079
080        @Override
081        public String toString() {
082                List<String> components = new ArrayList<>(4);
083
084                components.add(format("marshaledResponse=%s", getMarshaledResponse()));
085
086                Response response = getResponse().orElse(null);
087
088                if (response != null)
089                        components.add(format("response=%s", response));
090
091                CorsPreflightResponse corsPreflightResponse = getCorsPreflightResponse().orElse(null);
092
093                if (corsPreflightResponse != null)
094                        components.add(format("corsPreflightResponse=%s", corsPreflightResponse));
095
096                ResourceMethod resourceMethod = getResourceMethod().orElse(null);
097
098                if (resourceMethod != null)
099                        components.add(format("resourceMethod=%s", resourceMethod));
100
101                return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", ")));
102        }
103
104        @Override
105        public boolean equals(@Nullable Object object) {
106                if (this == object)
107                        return true;
108
109                if (!(object instanceof RequestResult requestResult))
110                        return false;
111
112                return Objects.equals(getMarshaledResponse(), requestResult.getMarshaledResponse())
113                                && Objects.equals(getResponse(), requestResult.getResponse())
114                                && Objects.equals(getCorsPreflightResponse(), requestResult.getCorsPreflightResponse())
115                                && Objects.equals(getResourceMethod(), requestResult.getResourceMethod());
116        }
117
118        @Override
119        public int hashCode() {
120                return Objects.hash(getMarshaledResponse(), getResponse(), getCorsPreflightResponse(), getResourceMethod());
121        }
122
123        /**
124         * The final representation of the response to be written over the wire.
125         *
126         * @return the response to be written over the wire
127         */
128        @Nonnull
129        public MarshaledResponse getMarshaledResponse() {
130                return this.marshaledResponse;
131        }
132
133        /**
134         * The logical response, determined by the return value of the <em>Resource Method</em> (if available).
135         *
136         * @return the logical response
137         */
138        @Nonnull
139        public Optional<Response> getResponse() {
140                return Optional.ofNullable(this.response);
141        }
142
143        /**
144         * The CORS preflight logical response, if applicable for the request.
145         *
146         * @return the CORS preflight logical response
147         */
148        @Nonnull
149        public Optional<CorsPreflightResponse> getCorsPreflightResponse() {
150                return Optional.ofNullable(this.corsPreflightResponse);
151        }
152
153        /**
154         * The <em>Resource Method</em> that handled the request, if available.
155         *
156         * @return the <em>Resource Method</em> that handled the request
157         */
158        @Nonnull
159        public Optional<ResourceMethod> getResourceMethod() {
160                return Optional.ofNullable(this.resourceMethod);
161        }
162
163        /**
164         * Builder used to construct instances of {@link RequestResult} via {@link RequestResult#withMarshaledResponse(MarshaledResponse)}.
165         * <p>
166         * This class is intended for use by a single thread.
167         *
168         * @author <a href="https://www.revetkn.com">Mark Allen</a>
169         */
170        @NotThreadSafe
171        public static class Builder {
172                @Nonnull
173                private MarshaledResponse marshaledResponse;
174                @Nullable
175                private Response response;
176                @Nullable
177                private CorsPreflightResponse corsPreflightResponse;
178                @Nullable
179                private ResourceMethod resourceMethod;
180
181                protected Builder(@Nonnull MarshaledResponse marshaledResponse) {
182                        requireNonNull(marshaledResponse);
183                        this.marshaledResponse = marshaledResponse;
184                }
185
186                @Nonnull
187                public Builder marshaledResponse(@Nonnull MarshaledResponse marshaledResponse) {
188                        requireNonNull(marshaledResponse);
189                        this.marshaledResponse = marshaledResponse;
190                        return this;
191                }
192
193                @Nonnull
194                public Builder response(@Nullable Response response) {
195                        this.response = response;
196                        return this;
197                }
198
199                @Nonnull
200                public Builder corsPreflightResponse(@Nullable CorsPreflightResponse corsPreflightResponse) {
201                        this.corsPreflightResponse = corsPreflightResponse;
202                        return this;
203                }
204
205                @Nonnull
206                public Builder resourceMethod(@Nullable ResourceMethod resourceMethod) {
207                        this.resourceMethod = resourceMethod;
208                        return this;
209                }
210
211                @Nonnull
212                public RequestResult build() {
213                        return new RequestResult(this);
214                }
215        }
216
217        /**
218         * Builder used to copy instances of {@link RequestResult} via {@link RequestResult#copy()}.
219         * <p>
220         * This class is intended for use by a single thread.
221         *
222         * @author <a href="https://www.revetkn.com">Mark Allen</a>
223         */
224        @NotThreadSafe
225        public static class Copier {
226                @Nonnull
227                private final Builder builder;
228
229                Copier(@Nonnull RequestResult requestResult) {
230                        requireNonNull(requestResult);
231
232                        this.builder = new Builder(requestResult.getMarshaledResponse())
233                                        .response(requestResult.getResponse().orElse(null))
234                                        .corsPreflightResponse(requestResult.getCorsPreflightResponse().orElse(null))
235                                        .resourceMethod(requestResult.getResourceMethod().orElse(null));
236                }
237
238                @Nonnull
239                public Copier marshaledResponse(@Nonnull MarshaledResponse marshaledResponse) {
240                        requireNonNull(marshaledResponse);
241                        this.builder.marshaledResponse(marshaledResponse);
242                        return this;
243                }
244
245                @Nonnull
246                public Copier response(@Nullable Response response) {
247                        this.builder.response(response);
248                        return this;
249                }
250
251                @Nonnull
252                public Copier corsPreflightResponse(@Nullable CorsPreflightResponse corsPreflightResponse) {
253                        this.builder.corsPreflightResponse(corsPreflightResponse);
254                        return this;
255                }
256
257                @Nonnull
258                public Copier resourceMethod(@Nullable ResourceMethod resourceMethod) {
259                        this.builder.resourceMethod(resourceMethod);
260                        return this;
261                }
262
263                @Nonnull
264                public RequestResult finish() {
265                        return this.builder.build();
266                }
267        }
268}