All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.bootique.junit5.PolymorphicConfigurationChecker Maven / Gradle / Ivy

There is a newer version: 3.0.M2
Show newest version
/*
 * Licensed to ObjectStyle LLC under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ObjectStyle LLC licenses
 * this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.bootique.junit5;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.bootique.config.PolymorphicConfiguration;
import io.bootique.config.TypesFactory;
import io.bootique.log.DefaultBootLogger;
import org.junit.jupiter.api.Assertions;

import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * @since 2.0
 */
public class PolymorphicConfigurationChecker {

    private Class expectedRoot;
    private Class expectedDefault;
    private Set> allExpectedTypes;

    @SafeVarargs
    protected PolymorphicConfigurationChecker(
            Class expectedRoot,
            Class expectedDefault,
            Class... otherConfigs) {

        this.expectedRoot = Objects.requireNonNull(expectedRoot);
        this.expectedDefault = expectedDefault;

        Set> allTypes = new HashSet<>();
        allTypes.add(Objects.requireNonNull(expectedRoot));

        if (expectedDefault != null) {
            allTypes.add(expectedDefault);
        }

        if (otherConfigs != null) {
            allTypes.addAll(Arrays.asList(otherConfigs));
        }

        this.allExpectedTypes = allTypes;
    }

    @SafeVarargs
    public static  void test(
            Class expectedRoot,
            Class expectedDefault,
            Class... otherConfigs) {
        new PolymorphicConfigurationChecker<>(expectedRoot, expectedDefault, otherConfigs).test();
    }

    @SafeVarargs
    public static  void testNoDefault(
            Class expectedRoot,
            Class... otherConfigs) {
        new PolymorphicConfigurationChecker<>(expectedRoot, null, otherConfigs).test();
    }

    protected void test() {

        Set> loaded = loadedFromSpi();
        assertEquals(allExpectedTypes, loaded, "Loaded and expected types do not match");

        testRoot();

        allExpectedTypes.forEach(t -> {

            if (!t.equals(expectedRoot)) {
                testNonRoot(t);
            }
        });
    }

    protected void testRoot() {
        // while boundaries are compiler-checked, let's still verify superclass, as generics in Java are easy to bypass
        assertTrue(PolymorphicConfiguration.class.isAssignableFrom(expectedRoot), "Invalid root type: " + expectedRoot);

        JsonTypeInfo typeInfo = expectedRoot.getAnnotation(JsonTypeInfo.class);

        //  TODO: test "property" and "use" values of the annotation
        assertNotNull(typeInfo,"Root is not annotated with @JsonTypeInfo");
        if (expectedDefault != null) {
            assertTrue(hasDefault(typeInfo),
                    "Default type is not specified on root. Expected: " + expectedDefault.getName());
            assertEquals(expectedDefault, typeInfo.defaultImpl(),
                    "Expected and actual default types are not the same");
        } else {
            assertFalse(hasDefault(typeInfo),
                    "Expected no default type, but @JsonTypeInfo sets it to " + typeInfo.defaultImpl().getName() + ".");
        }

        if (isConcrete(expectedRoot)) {
            JsonTypeName typeName = expectedRoot.getAnnotation(JsonTypeName.class);
            assertNotNull(typeName,"Concrete root configuration type must be annotated with @JsonTypeName: " + expectedRoot.getName());
        }
    }

    protected void testNonRoot(Class t) {
        // while boundaries are compiler-checked, let's still verify superclass, as generics in Java are easy to bypass
        assertTrue(expectedRoot.isAssignableFrom(t),
                "Invalid type " + t.getName() + ". Must be a subclass of root type " + expectedRoot.getName());

        assertTrue(isConcrete(t), "Non-root configuration type must not be abstract: " + t.getName());

        // this check would prevent matching subclasses by class, but we discourage that anyways.. (otherwise FQN
        // would have to be used in YAML)

        JsonTypeName typeName = t.getAnnotation(JsonTypeName.class);
        assertNotNull(typeName,"Non-root configuration type must be annotated with @JsonTypeName: " + t.getName());
    }


    protected Set> loadedFromSpi() {

        Collection> types;
        try {
            types = new TypesFactory<>(
                    getClass().getClassLoader(),
                    PolymorphicConfiguration.class,
                    new DefaultBootLogger(false)).getTypes();
        } catch (Exception e) {
            Assertions.fail(e.getMessage());
            // dead code; still required to compile...
            throw new RuntimeException(e);
        }

        return types.stream()
                .filter(p -> expectedRoot.isAssignableFrom(p))
                .collect(toSet());
    }

    protected boolean isConcrete(Class type) {
        int modifiers = type.getModifiers();
        return !Modifier.isAbstract(modifiers) && !Modifier.isInterface(modifiers);
    }

    protected boolean hasDefault(JsonTypeInfo typeInfo) {
        return !typeInfo.defaultImpl().equals(JsonTypeInfo.class);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy