1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.spring.test.api.comparator;
18
19 import static java.util.Objects.isNull;
20 import static java.util.Objects.nonNull;
21 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.ANNOTATION_MUST_NOT_BE_NULL;
22 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.CLASS_MUST_BE_INTERFACE;
23 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.METHOD_MUST_NOT_BE_NULL;
24 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_ANNOTATION_ATTRIBUTES_SIZE;
25 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_ANNOTATION_ATTRIBUTE_VALUE;
26 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_ANNOTATION_SIZE;
27 import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_METHOD_SIZE;
28 import static org.bremersee.spring.test.api.comparator.RestApiComparatorExclusion.isExcluded;
29 import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.ANNOTATION;
30 import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.ATTRIBUTE;
31 import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.CLASS;
32 import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.METHOD;
33 import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.METHOD_PARAMETER;
34 import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.pathBuilder;
35 import static org.springframework.util.ObjectUtils.isEmpty;
36
37 import java.lang.annotation.Annotation;
38 import java.lang.reflect.AnnotatedElement;
39 import java.lang.reflect.Method;
40 import java.lang.reflect.Parameter;
41 import java.util.Arrays;
42 import java.util.Map;
43 import java.util.stream.Collectors;
44 import org.assertj.core.api.SoftAssertions;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47 import org.springframework.core.annotation.AnnotationUtils;
48 import org.springframework.util.Assert;
49 import org.springframework.util.ReflectionUtils;
50 import org.springframework.util.StringUtils;
51
52
53
54
55
56
57 public class RestApiComparator {
58
59 private static final Logger log = LoggerFactory.getLogger(RestApiComparator.class);
60
61 private RestApiComparator() {
62 }
63
64
65
66
67
68
69
70
71 public static void assertSameApi(
72 final Class<?> expected,
73 final Class<?> actual,
74 final RestApiComparatorExclusion... exclusions) {
75
76 assertSameApi(null, true, expected, actual, exclusions);
77 }
78
79
80
81
82
83
84
85
86
87
88 public static void assertSameApi(
89 final SoftAssertions softAssertions,
90 final boolean assertAll,
91 final Class<?> expected,
92 final Class<?> actual,
93 final RestApiComparatorExclusion... exclusions) {
94
95 log.info("Assert same api: expected = {}, actual = {}", name(expected),
96 name(actual));
97 Assert.notNull(expected, "Expected api class must not be null.");
98 Assert.notNull(actual, "Actual api class must not be null.");
99
100 SoftAssertions softly;
101 boolean internalAssertAll;
102 if (nonNull(softAssertions)) {
103 softly = softAssertions;
104 internalAssertAll = assertAll;
105 } else {
106 softly = new SoftAssertions();
107 internalAssertAll = true;
108 }
109
110 RestApiComparatorPath path = pathBuilder().add(CLASS, expected.getSimpleName()).build();
111 if (!isExcluded(path, CLASS_MUST_BE_INTERFACE, exclusions)) {
112 log.info("""
113 Assert given class is an interface:
114 - path = {}
115 - type = {}""",
116 path, CLASS_MUST_BE_INTERFACE);
117 softly.assertThat(expected.isInterface())
118 .as("Expected api class must be an interface.")
119 .isTrue();
120 }
121
122 path = pathBuilder().add(CLASS, actual.getSimpleName()).build();
123 if (!isExcluded(path, CLASS_MUST_BE_INTERFACE, exclusions)) {
124 log.info("""
125 Assert given class is an interface:
126 - path = {}
127 - type = {}""",
128 path, CLASS_MUST_BE_INTERFACE);
129 softly.assertThat(actual.isInterface())
130 .as("Actual api class must be an interface.")
131 .isTrue();
132 }
133
134 assertSameClassAnnotations(softly, expected, actual, exclusions);
135 assertSameMethodAnnotations(softly, expected, actual, exclusions);
136
137 if (internalAssertAll) {
138 softly.assertAll();
139 }
140 }
141
142 private static void assertSameClassAnnotations(
143 SoftAssertions softly,
144 final Class<?> expected,
145 final Class<?> actual,
146 final RestApiComparatorExclusion... exclusions) {
147
148 final Annotation[] expectedAnnotations = expected.getAnnotations();
149 final Annotation[] actualAnnotations = actual.getAnnotations();
150 assertSameAnnotations(
151 softly,
152 pathBuilder().add(CLASS, actual.getSimpleName()).build(),
153 expectedAnnotations,
154 actualAnnotations,
155 actual,
156 exclusions);
157 }
158
159 private static void assertSameMethodAnnotations(
160 SoftAssertions softly,
161 final Class<?> expected,
162 final Class<?> actual,
163 final RestApiComparatorExclusion... exclusions) {
164
165 final RestApiComparatorPath classPath = pathBuilder().add(CLASS, actual.getSimpleName())
166 .build();
167 final Method[] expectedMethods = ReflectionUtils.getDeclaredMethods(expected);
168 final Method[] actualMethods = ReflectionUtils.getDeclaredMethods(actual);
169
170 if (!isExcluded(classPath, SAME_METHOD_SIZE, exclusions)) {
171 log.info("""
172 Assert same method size:
173 - path = {}
174 - type = {}""",
175 classPath, SAME_METHOD_SIZE);
176 softly.assertThat(actualMethods.length)
177 .as("Methods must have the same size on %s and %s.",
178 expected.getSimpleName(), actual.getSimpleName())
179 .isEqualTo(expectedMethods.length);
180 }
181
182 for (final Method expectedMethod : expectedMethods) {
183 final RestApiComparatorPath methodPath = classPath.toPathBuilder()
184 .add(METHOD, expectedMethod.getName())
185 .build();
186 final Method actualMethod = ReflectionUtils.findMethod(
187 actual, expectedMethod.getName(), expectedMethod.getParameterTypes());
188 if (!isExcluded(methodPath, METHOD_MUST_NOT_BE_NULL, exclusions)) {
189 log.info("""
190 Assert method exists:
191 - path = {}
192 - type = {}""",
193 methodPath, METHOD_MUST_NOT_BE_NULL);
194 softly.assertThat(actualMethod)
195 .as("Method %s (%s) must be present on %s", expectedMethod.getName(),
196 parameters(expectedMethod.getParameterTypes()), name(actual))
197 .isNotNull();
198 }
199 if (isNull(actualMethod)) {
200 continue;
201 }
202
203 final Annotation[] expectedAnnotations = expectedMethod.getAnnotations();
204 final Annotation[] actualAnnotations = actualMethod.getAnnotations();
205 assertSameAnnotations(
206 softly,
207 methodPath,
208 expectedAnnotations,
209 actualAnnotations,
210 actualMethod,
211 exclusions);
212
213 if (expectedMethod.getParameterCount() > 0) {
214 final Parameter[] expectedParameters = expectedMethod.getParameters();
215 final Parameter[] actualParameters = actualMethod.getParameters();
216 for (int i = 0; i < expectedParameters.length; i++) {
217 final Parameter expectedParameter = expectedParameters[i];
218 final Parameter actualParameter = actualParameters[i];
219 final RestApiComparatorPath methodParameterPath = methodPath.toPathBuilder()
220 .add(METHOD_PARAMETER, String.valueOf(i))
221 .build();
222 assertSameAnnotations(
223 softly,
224 methodParameterPath,
225 expectedParameter.getAnnotations(),
226 actualParameter.getAnnotations(),
227 actualParameter,
228 exclusions);
229 }
230 }
231 }
232 }
233
234 private static void assertSameAnnotations(
235 final SoftAssertions softly,
236 final RestApiComparatorPath path,
237 final Annotation[] expectedAnnotations,
238 final Annotation[] actualAnnotations,
239 final AnnotatedElement actualAnnotatedElement,
240 final RestApiComparatorExclusion... exclusions) {
241
242 if (!isExcluded(path, SAME_ANNOTATION_SIZE, exclusions)) {
243 log.info("""
244 Assert same annotation size:
245 - path = {}
246 - type = {}""",
247 path, SAME_ANNOTATION_SIZE);
248 softly.assertThat(actualAnnotations.length)
249 .as("Annotations must have the same size on %s.", path)
250 .isEqualTo(expectedAnnotations.length);
251 }
252 for (final Annotation expectedAnnotation : expectedAnnotations) {
253 final Annotation actualAnnotation = AnnotationUtils.getAnnotation(
254 actualAnnotatedElement, expectedAnnotation.annotationType());
255 assertSameAnnotation(
256 softly,
257 path,
258 expectedAnnotation,
259 actualAnnotation,
260 exclusions);
261 }
262 }
263
264 private static void assertSameAnnotation(
265 final SoftAssertions softly,
266 final RestApiComparatorPath path,
267 final Annotation expected,
268 final Annotation actual,
269 final RestApiComparatorExclusion... exclusions) {
270
271 final RestApiComparatorPath annotationPath = path.toPathBuilder()
272 .add(ANNOTATION, expected.annotationType().getSimpleName())
273 .build();
274 if (!isExcluded(annotationPath, ANNOTATION_MUST_NOT_BE_NULL, exclusions)) {
275 log.info("""
276 Assert annotation is not null:
277 - path = {}
278 - type = {}""",
279 annotationPath, ANNOTATION_MUST_NOT_BE_NULL);
280 softly.assertThat(actual)
281 .as("Annotation %s is missing on %s.", name(expected), path)
282 .isNotNull();
283 }
284 if (isNull(actual)) {
285 return;
286 }
287 final Map<String, Object> expectedAttributes = AnnotationUtils.getAnnotationAttributes(
288 expected, true, false);
289 final Map<String, Object> actualAttributes = AnnotationUtils.getAnnotationAttributes(
290 actual, true, false);
291 assertSameAttributes(
292 softly,
293 annotationPath,
294 expectedAttributes,
295 actualAttributes,
296 exclusions);
297 }
298
299 private static void assertSameAttributes(
300 final SoftAssertions softly,
301 final RestApiComparatorPath annotationPath,
302 final Map<String, Object> expected,
303 final Map<String, Object> actual,
304 final RestApiComparatorExclusion... exclusions) {
305
306 if (!isExcluded(annotationPath, SAME_ANNOTATION_ATTRIBUTES_SIZE, exclusions)) {
307 log.info("""
308 Assert same annotation attributes size:
309 - path = {}
310 - type = {}""",
311 annotationPath, SAME_ANNOTATION_ATTRIBUTES_SIZE);
312 softly.assertThat(actual.size())
313 .as("Attributes of annotation (%s) must have the same size.", annotationPath)
314 .isEqualTo(expected.size());
315 }
316 for (final Map.Entry<String, Object> expectedAttribute : expected.entrySet()) {
317 final Object expectedAttributeValue = expectedAttribute.getValue();
318 final Object actualAttributeValue = actual.get(expectedAttribute.getKey());
319 assertSameAttributeValues(
320 softly,
321 annotationPath.toPathBuilder().add(ATTRIBUTE, expectedAttribute.getKey()).build(),
322 expectedAttributeValue,
323 actualAttributeValue,
324 exclusions);
325 }
326 }
327
328 private static void assertSameAttributeValues(
329 final SoftAssertions softly,
330 final RestApiComparatorPath attributePath,
331 final Object expected,
332 final Object actual,
333 final RestApiComparatorExclusion... exclusions) {
334
335 if (!isExcluded(attributePath, SAME_ANNOTATION_ATTRIBUTE_VALUE, exclusions)) {
336 log.info("""
337 Assert same attribute values:
338 - path = {}
339 - expected = {}
340 - actual = {}
341 - type = {}""",
342 attributePath, expected, actual, SAME_ANNOTATION_ATTRIBUTE_VALUE);
343
344 if (isNull(exclusions)) {
345 softly.assertThat(actual)
346 .as("Attribute (%s) must be null.", attributePath)
347 .isNull();
348 } else if (expected instanceof Annotation[] expectedAnnotations) {
349 softly.assertThat(actual)
350 .as("Attributes must be instance of 'Annotation[]' on %s", attributePath)
351 .isInstanceOf(Annotation[].class);
352 final Annotation[] actualAnnotations = (Annotation[]) actual;
353 softly.assertThat(actualAnnotations.length)
354 .as("Annotations must have same size on %s", attributePath)
355 .isEqualTo(expectedAnnotations.length);
356 for (int i = 0; i < actualAnnotations.length; i++) {
357 final Annotation expectedAnnotation = expectedAnnotations[i];
358 final Annotation actualAnnotation = actualAnnotations[i];
359 assertSameAnnotation(
360 softly,
361 attributePath,
362 expectedAnnotation,
363 actualAnnotation,
364 exclusions);
365 }
366 } else if (expected instanceof Annotation expectedAnnotation) {
367 final Annotation actualAnnotation = (Annotation) actual;
368 assertSameAnnotation(
369 softly,
370 attributePath,
371 expectedAnnotation,
372 actualAnnotation,
373 exclusions);
374 } else {
375 if (expected.getClass().isArray()) {
376 final Object[] expectedArray = (Object[]) expected;
377 final Object[] actualArray = (Object[]) actual;
378 softly.assertThat(actualArray)
379 .as("Arrays must be equal on %s", attributePath)
380 .isEqualTo(expectedArray);
381 } else {
382 softly.assertThat(actual)
383 .as("Objects must be equal on %s", attributePath)
384 .isEqualTo(expected);
385 }
386 }
387 }
388 }
389
390 private static String name(final Annotation annotation) {
391 return nonNull(annotation) ? name(annotation.annotationType()) : "null";
392 }
393
394 private static String name(final Class<?> clazz) {
395 return nonNull(clazz) ? clazz.getName() : "null";
396 }
397
398 private static String parameters(final Class<?>[] parameters) {
399 if (isEmpty(parameters)) {
400 return "";
401 }
402 return StringUtils.collectionToCommaDelimitedString(Arrays.stream(parameters)
403 .map(RestApiComparator::name).collect(Collectors.toList()));
404 }
405
406 }