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

com.tngtech.archunit.library.dependencies.Slices Maven / Gradle / Ivy

/*
 * Copyright 2019 TNG Technology Consulting GmbH
 *
 * Licensed 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 com.tngtech.archunit.library.dependencies;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.common.base.Joiner;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.DescribedIterable;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.Guava;
import com.tngtech.archunit.base.Optional;
import com.tngtech.archunit.base.PackageMatcher;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.properties.CanOverrideDescription;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ClassesTransformer;
import com.tngtech.archunit.lang.syntax.PredicateAggregator;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.base.PackageMatcher.TO_GROUPS;
import static com.tngtech.archunit.core.domain.Dependency.toTargetClasses;

/**
 * Basic collection of {@link Slice} for tests of dependencies between different domain packages, e.g. to avoid cycles.
 * Refer to {@link SlicesRuleDefinition} for further info on how to form an {@link ArchRule} to test slices.
 */
public final class Slices implements DescribedIterable, CanOverrideDescription {
    private final Iterable slices;
    private final String description;

    private Slices(Iterable slices) {
        this(slices, "Slices");
    }

    private Slices(Iterable slices, String description) {
        this.slices = slices;
        this.description = description;
    }

    @Override
    public Iterator iterator() {
        return slices.iterator();
    }

    @Override
    public Slices as(String description) {
        return new Slices(slices, description);
    }

    @Override
    public String getDescription() {
        return description;
    }

    /**
     * Allows the naming of single slices, where back references to the matching pattern can be denoted by '$' followed
     * by capturing group number. 
* E.g. {@code namingSlices("Slice $1")} would name a slice matching {@code '*..service.(*)..*'} * against {@code 'com.some.company.service.hello.something'} as 'Slice hello'.
* Likewise, if the slices were created by a {@link SliceAssignment} (compare * {@link #assignedFrom(SliceAssignment)}), * then the back reference refers to the n-th element of the identifier. * * @param pattern The naming pattern, e.g. 'Slice $1' * @return New (equivalent) slices with adjusted description for each single slice */ @PublicAPI(usage = ACCESS) public Slices namingSlices(String pattern) { List newSlices = new ArrayList<>(); for (Slice slice : slices) { newSlices.add(slice.as(pattern)); } return new Slices(newSlices, description); } /** * Supports partitioning a set of {@link JavaClasses} into different slices by matching the supplied * package identifier. For identifier syntax, see {@link PackageMatcher}.
* The slicing is done according to capturing groups (thus if none are contained in the identifier, no more than * a single slice will be the result). For example *

* Suppose there are three classes:

* {@code com.example.slice.one.SomeClass}
* {@code com.example.slice.one.AnotherClass}
* {@code com.example.slice.two.YetAnotherClass}

* If slices are created by specifying

* {@code Slices.of(classes).byMatching("..slice.(*)..")}

* then the result will be two slices, the slice where the capturing group is 'one' and the slice where the * capturing group is 'two'. *

* * @param packageIdentifier The identifier to match against * @return Slices partitioned according the supplied package identifier */ @PublicAPI(usage = ACCESS) public static Transformer matching(String packageIdentifier) { PackageMatchingSliceIdentifier sliceIdentifier = new PackageMatchingSliceIdentifier(packageIdentifier); String description = "slices matching " + sliceIdentifier.getDescription(); return new Transformer(sliceIdentifier, description); } /** * Supports partitioning a set of {@link JavaClasses} into different {@link Slices} by the supplied * {@link SliceAssignment}. This is basically a mapping {@link JavaClass} -> {@link SliceIdentifier}, * i.e. if the {@link SliceAssignment} returns the same * {@link SliceIdentifier} for two classes they will end up in the same slice. * A {@link JavaClass} will be ignored within the slices, if its {@link SliceIdentifier} is * {@link SliceIdentifier#ignore()}. For example *

* Suppose there are four classes:

* {@code com.somewhere.SomeClass}
* {@code com.somewhere.AnotherClass}
* {@code com.other.elsewhere.YetAnotherClass}
* {@code com.randomly.anywhere.AndYetAnotherClass}

* If slices are created by specifying

* {@code Slices.of(classes).assignedFrom(customAssignment)}

* * and the {@code customAssignment} maps

* * {@code com.somewhere -> SliceIdentifier.of("somewhere")}
* {@code com.other.elsewhere -> SliceIdentifier.of("elsewhere")}
* {@code com.randomly -> SliceIdentifier.ignore()}

* then the result will be two slices, identified by the single strings 'somewhere' (containing {@code SomeClass} * and {@code AnotherClass}) and 'elsewhere' (containing {@code YetAnotherClass}). The class {@code AndYetAnotherClass} * will be missing from all slices. * * @param sliceAssignment The assignment of {@link JavaClass} to {@link SliceIdentifier} * @return Slices partitioned according the supplied assignment */ @PublicAPI(usage = ACCESS) public static Transformer assignedFrom(SliceAssignment sliceAssignment) { String description = "slices assigned from " + sliceAssignment.getDescription(); return new Transformer(sliceAssignment, description); } /** * Specifies how to transform a set of {@link JavaClass} into a set of {@link Slice}, e.g. to test that * no cycles between certain package slices appear. * * @see Slices */ public static class Transformer implements ClassesTransformer { private final SliceAssignment sliceAssignment; private final String description; private final Optional namingPattern; private final SlicesPredicateAggregator predicate; Transformer(SliceAssignment sliceAssignment, String description) { this(sliceAssignment, description, new SlicesPredicateAggregator("that")); } private Transformer(SliceAssignment sliceAssignment, String description, SlicesPredicateAggregator predicate) { this(sliceAssignment, description, Optional.absent(), predicate); } private Transformer(SliceAssignment sliceAssignment, String description, Optional namingPattern, SlicesPredicateAggregator predicate) { this.sliceAssignment = checkNotNull(sliceAssignment); this.description = checkNotNull(description); this.namingPattern = checkNotNull(namingPattern); this.predicate = predicate; } /** * @see Slices#namingSlices(String) */ Transformer namingSlices(String pattern) { return namingSlices(Optional.of(pattern)); } private Transformer namingSlices(Optional pattern) { return new Transformer(sliceAssignment, description, pattern, predicate); } @Override public Transformer as(String description) { return new Transformer(sliceAssignment, description, predicate).namingSlices(namingPattern); } public Slices of(JavaClasses classes) { return new Slices(transform(classes)); } public Slices transform(Iterable dependencies) { return new Slices(transform(toTargetClasses(dependencies))); } @Override public Slices transform(JavaClasses classes) { Slices slices = createSlices(classes); if (namingPattern.isPresent()) { slices = slices.namingSlices(namingPattern.get()); } if (predicate.isPresent()) { slices = new Slices(Guava.Iterables.filter(slices, predicate.get())); } return slices.as(getDescription()); } private Slices createSlices(JavaClasses classes) { SliceBuilders sliceBuilders = new SliceBuilders(sliceAssignment); for (JavaClass clazz : classes) { sliceBuilders.add(clazz); } return new Slices(sliceBuilders.build()); } @Override public Slices.Transformer that(final DescribedPredicate predicate) { String newDescription = this.predicate.joinDescription(getDescription(), predicate.getDescription()); return new Transformer(sliceAssignment, newDescription, namingPattern, this.predicate.add(predicate)); } @Override public String getDescription() { return description; } Transformer thatANDsPredicates() { return new Transformer(sliceAssignment, description, namingPattern, predicate.thatANDs()); } Transformer thatORsPredicates() { return new Transformer(sliceAssignment, description, namingPattern, predicate.thatORs()); } } // Since Slices can be renamed with 'as' in the middle (e.g. slices().that(foo).as("bar").should()... -> "bar should") // we need this workaround for now private static class SlicesPredicateAggregator { private final PredicateAggregator predicate; private final String descriptionJoinWord; SlicesPredicateAggregator(String descriptionJoinWord) { this(new PredicateAggregator(), descriptionJoinWord); } private SlicesPredicateAggregator(PredicateAggregator predicate, String descriptionJoinWord) { this.predicate = checkNotNull(predicate); this.descriptionJoinWord = checkNotNull(descriptionJoinWord); } boolean isPresent() { return predicate.isPresent(); } DescribedPredicate get() { return predicate.get(); } SlicesPredicateAggregator add(DescribedPredicate predicate) { return new SlicesPredicateAggregator(this.predicate.add(predicate), descriptionJoinWord); } SlicesPredicateAggregator thatANDs() { return new SlicesPredicateAggregator(predicate.thatANDs(), "and"); } SlicesPredicateAggregator thatORs() { return new SlicesPredicateAggregator(predicate.thatORs(), "or"); } String joinDescription(String first, String second) { return Joiner.on(" ").join(first, descriptionJoinWord, second); } } private static class SliceBuilders { private final Map, Slice.Builder> sliceBuilders = new HashMap<>(); private final SliceAssignment sliceAssignment; SliceBuilders(SliceAssignment sliceAssignment) { this.sliceAssignment = sliceAssignment; } void add(JavaClass clazz) { List identifierParts = sliceAssignment.getIdentifierOf(clazz).getParts(); if (identifierParts.isEmpty()) { return; } if (!sliceBuilders.containsKey(identifierParts)) { sliceBuilders.put(identifierParts, Slice.Builder.from(identifierParts, sliceAssignment)); } sliceBuilders.get(identifierParts).addClass(clazz); } Set build() { Set result = new HashSet<>(); for (Slice.Builder builder : sliceBuilders.values()) { result.add(builder.build()); } return result; } } private static class PackageMatchingSliceIdentifier implements SliceAssignment { private final String packageIdentifier; private PackageMatchingSliceIdentifier(String packageIdentifier) { this.packageIdentifier = checkNotNull(packageIdentifier); } @Override public SliceIdentifier getIdentifierOf(JavaClass javaClass) { PackageMatcher matcher = PackageMatcher.of(packageIdentifier); Optional> result = matcher.match(javaClass.getPackageName()).transform(TO_GROUPS); List parts = result.or(Collections.emptyList()); return parts.isEmpty() ? SliceIdentifier.ignore() : SliceIdentifier.of(parts); } @Override public String getDescription() { return slicesMatchingDescription(packageIdentifier); } private static String slicesMatchingDescription(String packageIdentifier) { return "'" + packageIdentifier + "'"; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy