Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict scanning for concrete types over specific packages #471

Open
npepinpe opened this issue Feb 20, 2022 · 2 comments · May be fixed by #474
Open

Restrict scanning for concrete types over specific packages #471

npepinpe opened this issue Feb 20, 2022 · 2 comments · May be fixed by #474
Labels
Milestone

Comments

@npepinpe
Copy link
Contributor

Class scanning is a great feature, but depending on the size of your project, it leads to some minor issues. For example, I'd like to use EasyRandom as part of the test harness for a project. The test harness has its own module as it gets included into different module. Scanning for types may end up finding different implementations of the interfaces depending on the module in which the test harness is pulled. I'm sure that's a nice use case for some, but it can also lead to some issues. For example, the test harness module already pulls in a dependency which has concrete implementations of some types. These are beans and can easily be generated by EasyRandom. However, maybe in another module, there are other implementations which run into limitations of EasyRandom, or which aren't strictly beans. In this case, it would be great if I could specify which implementations to use.

Secondly, scanning the whole class path can be prohibitively expensive. This isn't a big overhead when running a complete test suite, but when running single tests in your IDE having it add a few seconds for a test which takes 5 milliseconds is a bit tedious.

Now, neither are blockers, but restricting the scanning to specific packages seems like a pretty easy fix - this is already supported by ClassGraph, and it would fix both issues.

Let me know if I wasn't clear. If this is something you'd consider, I'm also happy opening the PR myself.

@npepinpe
Copy link
Contributor Author

npepinpe commented Mar 2, 2022

I would propose something slightly different, in the end. Introduce a new interface in the API:

/**
 * Interface describing the API to resolve concrete types for a given abstract type.
 *
 * For example, a resolver which resolves all {@link java.util.List} to {@link java.util.ArrayList},
 * and delegates other types to the default resolver:
 *
 * <pre>{@code
 * public class ListConcreteTypeResolver implements ConcreteTypeResolver {
 *   <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type) {
 *     if (List.class.equals(type)) {
 *       return ArrayList.class;
 *     }
 *
 *     return ConcreteTypeResolver.defaultConcreteTypeResolver().getPublicConcreteSubTypesOf(type);
 *   }
 * }
 * }</pre>
 */
@FunctionalInterface
public interface ConcreteTypeResolver {

  /**
   * Returns a list of concrete types for the given {@code type}.
   *
   * @param type the abstract type to resolve
   * @param <T> the actual type to introspect
   * @return a list of all concrete subtypes to use
   */
  <T> List<Class<?>> getPublicConcreteSubTypesOf(final Class<T> type);

  /**
   * @return a default concrete type resolver which will scan the whole classpath for concrete types
   */
  static ConcreteTypeResolver defaultConcreteTypeResolver() {
    return ReflectionUtils::getPublicConcreteSubTypesOf;
  }
}

This interface's default implementation is then the current type scanning mechanism, i.e. the one which relies on ClassGraph. By having this simple interface, users can submit their own ways of scanning for concrete type, which could be reusing ClassGraph but with various filters. This avoids coupling easy-random to ClassGraph via its public API.

I'll open a PR, let me know what you think. I know technically this is a new feature, and I understand the project is in maintenance mode, but I hope it's small enough you might consider it 🙂

@npepinpe npepinpe linked a pull request Mar 2, 2022 that will close this issue
@npepinpe
Copy link
Contributor Author

npepinpe commented Mar 2, 2022

For those wondering, the current workaround is to register a randomizer for every abstract type, mapping to a list of known concrete types. So you could reuse ClassGraph yourself, for example, if you had two types, com.acme.super.cool.package.MyAbstractType and its implementation, com.acme.super.cool.package.impl.MyConcreteType:

final CustomRandomizerRegistry randomizerRegistry = new CustomRandomizerRegistry();
final EasyRandom random = new EasyRandom(new EasyRandomParameters().randomizerRegistry(randomizerRegistry));

// the .. at the end specifies to look at all sub-packages as well
final ScanResult result = new ClassGraph().acceptPackages("com.acme.super.cool.package..").scan();
// scan all interfaces; this doesn't include abstract classes, but it shouldn't be hard to add
result.getAllInterfaces().forEach(abstractInfo -> {
  // look at all concrete classes directly implementing this interface
  abstractInfo
    .getClassesImplementing()
    .filter(ClassInfo::isStandardClass)
    .filter(info -> !info.isAbstract())
    .directOnly()
    .forEach(concreteInfo -> {
      randomizerRegistry.registerRandomizer(
        abstractInfo.loadClass(), 
        concreteInfo.loadClass(abstractInfo.loadClass())
      );
  });
});

// this will create an instance of MyConcreteType
final MyAbstractType instance = random.nextObject(MyAbstractType.class);

The downside with this workaround is that you rely on the custom randomizers referring to the EasyRandom instance in a circular way, which isn't super great. But it works. Hope this helps someone else!

@fmbenhassine fmbenhassine added this to the 6.0.0 milestone Sep 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants