001/*
002 * MIT License
003 *
004 * Copyright (c) 2023 Michael Cowan
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024
025package io.blt.test.assertj;
026
027import java.lang.annotation.Annotation;
028import java.lang.reflect.Field;
029import java.lang.reflect.Method;
030import java.util.Arrays;
031import org.assertj.core.api.ObjectAssert;
032import org.assertj.core.internal.Failures;
033
034import static org.assertj.core.api.Assertions.assertThat;
035
036/**
037 * Assertion factory methods that allow testing the annotations of various types.
038 * <p>
039 * Each method is a static factory for an annotation specific assertion object
040 * i.e. each method returns {@code ObjectAssert<T extends Annotation>}.
041 * </p>
042 * e.g.
043 * <pre>{@code
044 * @Test
045 * void isAnnotatedAsTransactionalWithNoRollbackForException() {
046 *     assertHasAnnotation(NotificationPublisher.class, Transactional.class)
047 *             .extracting(Transactional::noRollbackFor)
048 *             .isEqualTo(Exception.class);
049 * }
050 * }</pre>
051 */
052public final class AnnotationAssertions {
053
054    private AnnotationAssertions() {
055        throw new IllegalAccessError("Utility class should be accessed statically and never constructed");
056    }
057
058    /**
059     * Asserts that a {@code Method} is annotated with a given annotation.
060     * <p>
061     * If present, an assertion object is returned for the found annotation instance, else the test fails.
062     * </p>
063     * e.g.
064     * <pre>{@code
065     * @Test
066     * void isAnnotatedAsTransactionalWithNoRollbackForException() throws Exception {
067     *     var method = NotificationPublisher.class.getMethod("sendNotification");
068     *
069     *     assertHasAnnotation(method, Transactional.class)
070     *             .extracting(Transactional::noRollbackFor)
071     *             .isEqualTo(Exception.class);
072     * }
073     * }</pre>
074     *
075     * @param method     a {@code Method} to test for the presence of {@code annotation}
076     * @param annotation the expected {@code Annotation} type
077     * @param <T>        type of {@code Annotation}
078     * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>}
079     */
080    public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Method method, Class<T> annotation) {
081        return assertThat(findAnnotationOfTypeOrFail(method.getAnnotations(), annotation));
082    }
083
084    /**
085     * Asserts that a {@code Class} is annotated with a given annotation.
086     * <p>
087     * If present, an assertion object is returned for the found annotation instance, else the test fails.
088     * </p>
089     * e.g.
090     * <pre>{@code
091     * @Test
092     * void isAnnotatedAsTransactionalWithNoRollbackForException() {
093     *     assertHasAnnotation(NotificationPublisher.class, Transactional.class)
094     *             .extracting(Transactional::noRollbackFor)
095     *             .isEqualTo(Exception.class);
096     * }
097     * }</pre>
098     *
099     * @param clazz      a {@code Class} to test for the presence of {@code annotation}
100     * @param annotation the expected {@code Annotation} type
101     * @param <T>        type of {@code Annotation}
102     * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>}
103     */
104    public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Class<?> clazz, Class<T> annotation) {
105        return assertThat(findAnnotationOfTypeOrFail(clazz.getAnnotations(), annotation));
106    }
107
108    /**
109     * Asserts that a {@code Field} is annotated with a given annotation.
110     * <p>
111     * If present, an assertion object is returned for the found annotation instance, else the test fails.
112     * </p>
113     * e.g.
114     * <pre>{@code
115     * @Test
116     * void isAnnotatedAsDigitsWithFractionOfTwo() {
117     *     var method = Invoice.class.getField("price");
118     *
119     *     assertHasAnnotation(field, Digits.class)
120     *             .extracting(Digits::fraction)
121     *             .isEqualTo(2);
122     * }
123     * }</pre>
124     *
125     * @param field      a {@code Field} to test for the presence of {@code annotation}
126     * @param annotation the expected {@code Annotation} type
127     * @param <T>        type of {@code Annotation}
128     * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>}
129     */
130    public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Field field, Class<T> annotation) {
131        return assertThat(findAnnotationOfTypeOrFail(field.getAnnotations(), annotation));
132    }
133
134    private static <T extends Annotation> T findAnnotationOfTypeOrFail(Annotation[] annotations, Class<T> type) {
135        var instance = Arrays.stream(annotations)
136                             .filter(a -> a.annotationType().equals(type))
137                             .findFirst()
138                             .orElseThrow(() -> Failures.instance().failure(
139                                     "Cannot find annotation of type " + type.getSimpleName()));
140
141        return type.cast(instance);
142    }
143
144}