Repository: avaje/avaje-inject
Pull request #194 (2022-04-05 04:30:34)

#193 - Add @InjectModule requirePackages ... for better multi-module requires/provides
This means that we don't need to reference each and every dependency in requires and instead can put one
into requiresPackages such that any dependency in that package or a subpackage of that package is deemed
to be supplied by another module.


Patch:
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index 586fe73c..0df9bbe4 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>8.0</version>
+    <version>8.1.RC1</version>
   </parent>
 
   <artifactId>avaje-inject-generator</artifactId>
@@ -16,7 +16,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-inject</artifactId>
-      <version>8.0</version>
+      <version>8.1.RC1</version>
     </dependency>
 
     <!-- test dependencies -->
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java b/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
index 8debe04c..d185487c 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
@@ -14,6 +14,11 @@ final class Dependency {
     this.softDependency = softDependency;
   }
 
+  @Override
+  public String toString() {
+    return name;
+  }
+
   String getName() {
     return name;
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
index 9c668fe5..78675c20 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
@@ -38,7 +38,7 @@ class MetaDataOrdering {
   }
 
   /**
-   * These if defined are expected to be required at wiring time probably via another module.
+   * These, if defined are expected to be required at wiring time probably via another module.
    */
   private void externallyRequiredDependencies() {
     for (String requireType : scopeInfo.requires()) {
@@ -121,8 +121,7 @@ class MetaDataOrdering {
 
   private void checkMissingDependencies(MetaData metaData) {
     for (Dependency dependency : metaData.getDependsOn()) {
-      if (providers.get(dependency.getName()) == null
-        && !scopeInfo.providedByOtherModule(dependency.getName())) {
+      if (providers.get(dependency.getName()) == null && !scopeInfo.providedByOtherModule(dependency.getName())) {
         TypeElement element = context.elementMaybe(metaData.getType());
         context.logError(element, "No dependency provided for " + dependency + " on " + metaData.getType());
         missingDependencyTypes.add(dependency.getName());
@@ -176,7 +175,7 @@ class MetaDataOrdering {
         // check non-provider dependency is satisfied
         ProviderList providerList = providers.get(dependency.getName());
         if (providerList == null) {
-          if (!scopeInfo.providedByOtherModule(dependency.getName()) && !dependency.isSoftDependency()) {
+          if (!scopeInfo.providedByOther(dependency)) {
             return false;
           }
         } else {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
index 03c4e8a0..b20fef2b 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
@@ -47,6 +47,8 @@ class ScopeInfo {
   private final ProcessingContext context;
   private final Set<String> requires = new LinkedHashSet<>();
   private final Set<String> provides = new LinkedHashSet<>();
+  private final Set<String> requiresPackages = new LinkedHashSet<>();
+  private final List<String> requirePkg = new ArrayList<>();
   private final boolean defaultScope;
   private final TypeElement annotationType;
   private final AllScopes scopes;
@@ -100,6 +102,10 @@ class ScopeInfo {
   private void read(Element element) {
     requires(ScopeUtil.readRequires(element));
     provides(ScopeUtil.readProvides(element));
+    for (String require : ScopeUtil.readRequiresPackages(element)) {
+      requiresPackages.add(require);
+      requirePkg.add(Util.packageOf(require) + ".");
+    }
   }
 
   private String initName(String topPackage) {
@@ -160,10 +166,6 @@ class ScopeInfo {
     return requires;
   }
 
-  Set<String> provides() {
-    return provides;
-  }
-
   void writeBeanHelpers() {
     for (BeanReader beanReader : beanReaders) {
       try {
@@ -333,6 +335,10 @@ class ScopeInfo {
       attributeClasses(leadingComma, writer, "requires", requires);
       leadingComma = true;
     }
+    if (!requiresPackages.isEmpty()) {
+      attributeClasses(leadingComma, writer, "requiresPackages", requiresPackages);
+      leadingComma = true;
+    }
     if (annotationType != null) {
       if (leadingComma) {
         writer.append(", ");
@@ -379,6 +385,9 @@ class ScopeInfo {
     writer.append("  private final Class<?>[] requires = ");
     buildClassArray(writer, requires);
     writer.append(";").eol();
+    writer.append("  private final Class<?>[] requiresPackages = ");
+    buildClassArray(writer, requiresPackages);
+    writer.append(";").eol();
     writer.append("  private Builder builder;").eol().eol();
   }
 
@@ -458,6 +467,21 @@ class ScopeInfo {
     return false;
   }
 
+  boolean providedByPackage(String dependency) {
+    for (String pkg : requirePkg) {
+      if (dependency.startsWith(pkg)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  boolean providedByOther(Dependency dependency) {
+    return dependency.isSoftDependency()
+      || providedByPackage(dependency.getName())
+      || providedByOtherModule(dependency.getName());
+  }
+
   Set<String> initModuleDependencies(Set<String> importTypes) {
     if (defaultScope || requires.isEmpty()) {
       return importTypes;
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
index 61d39d39..b53231ef 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
@@ -11,11 +11,15 @@ class ScopeUtil {
   private static final String INJECT_MODULE = "io.avaje.inject.InjectModule";
 
   static List<String> readProvides(Element element) {
-    return readClasses(element, "provides");
+    return readClasses(element, "provides(");
   }
 
   static List<String> readRequires(Element element) {
-    return readClasses(element, "requires");
+    return readClasses(element, "requires(");
+  }
+
+  static List<String> readRequiresPackages(Element element) {
+    return readClasses(element, "requiresPackages(");
   }
 
   static List<String> readClasses(Element element, String attributeName) {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
index c04f7df6..52fd9a9a 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
@@ -185,6 +185,11 @@ class SimpleModuleWriter {
     writer.append("  public Class<?>[] requires() {").eol();
     writer.append("    return requires;").eol();
     writer.append("  }").eol().eol();
+
+    writer.append("  @Override").eol();
+    writer.append("  public Class<?>[] requiresPackages() {").eol();
+    writer.append("    return requiresPackages;").eol();
+    writer.append("  }").eol().eol();
   }
 
   private void writeWithBeans() {
diff --git a/inject/pom.xml b/inject/pom.xml
index a9f74792..8b6c56df 100644
--- a/inject/pom.xml
+++ b/inject/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>8.0</version>
+    <version>8.1.RC1</version>
   </parent>
 
   <artifactId>avaje-inject</artifactId>
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 9be4d875..57c01b76 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -191,32 +191,28 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       }
     }
 
-    void add(Module factory) {
-      FactoryState wrappedFactory = new FactoryState(factory);
-      providesMap.computeIfAbsent(factory.getClass().getTypeName(), s -> new FactoryList()).add(wrappedFactory);
-      if (!isEmpty(factory.provides())) {
-        for (Class<?> feature : factory.provides()) {
-          providesMap.computeIfAbsent(feature.getTypeName(), s -> new FactoryList()).add(wrappedFactory);
+    void add(Module module) {
+      FactoryState factoryState = new FactoryState(module);
+      providesMap.computeIfAbsent(module.getClass().getTypeName(), s -> new FactoryList()).add(factoryState);
+      if (!factoryState.isProvidesEmpty()) {
+        for (Class<?> feature : module.provides()) {
+          providesMap.computeIfAbsent(feature.getTypeName(), s -> new FactoryList()).add(factoryState);
         }
       }
-      if (isEmpty(factory.requires())) {
-        if (!isEmpty(factory.provides())) {
+      if (factoryState.isRequiresEmpty()) {
+        if (factoryState.isProvidesEmpty()) {
           // only has 'provides' so we can push this
-          push(wrappedFactory);
+          push(factoryState);
         } else {
           // hold until after all the 'provides only' modules are added
-          queueNoDependencies.add(wrappedFactory);
+          queueNoDependencies.add(factoryState);
         }
       } else {
         // queue it to process by dependency ordering
-        queue.add(wrappedFactory);
+        queue.add(factoryState);
       }
     }
 
-    private boolean isEmpty(@Nullable Class<?>[] values) {
-      return values == null || values.length == 0;
-    }
-
     /**
      * Push the factory onto the build order (the wiring order for modules).
      */
@@ -250,7 +246,6 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
      * Process the queue pushing the factories in order to satisfy dependencies.
      */
     private void processQueue() {
-
       int count;
       do {
         count = processQueuedFactories();
@@ -262,24 +257,32 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
         for (FactoryState factoryState : queue) {
           push(factoryState);
         }
-
       } else if (!queue.isEmpty()) {
         StringBuilder sb = new StringBuilder();
         for (FactoryState factory : queue) {
-          sb.append("Module [").append(factory.getClass()).append("] has unsatisfied dependencies on modules:");
+          sb.append("Module [").append(factory).append("] has unsatisfied");
           for (Class<?> depModuleName : factory.requires()) {
-            if (!moduleNames.contains(depModuleName.getName())) {
-              sb.append(String.format(" [%s]", depModuleName));
+            if (notProvided(depModuleName.getTypeName())) {
+              sb.append(String.format(" requires [%s]", depModuleName.getTypeName()));
+            }
+          }
+          for (Class<?> depModuleName : factory.requiresPackages()) {
+            if (notProvided(depModuleName.getTypeName())) {
+              sb.append(String.format(" requiresPackages [%s]", depModuleName.getTypeName()));
             }
           }
         }
-
-        sb.append(". Modules that were loaded ok are:").append(moduleNames);
-        sb.append(". Maybe need to add external dependencies via BeanScopeBuilder.withBean()?");
+        sb.append(" - none of the loaded modules ").append(moduleNames).append(" have this in their @InjectModule( provides = ... ). ");
+        sb.append("Either @InjectModule requires/provides are not aligned? or add external dependencies via BeanScopeBuilder.withBean()?");
         throw new IllegalStateException(sb.toString());
       }
     }
 
+    private boolean notProvided(String dependency) {
+      FactoryList factories = providesMap.get(dependency);
+      return (factories == null || !factories.allPushed());
+    }
+
     /**
      * Process the queued factories pushing them when all their (module) dependencies
      * are satisfied.
@@ -305,9 +308,13 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
      * Return true if the (module) dependencies are satisfied for this factory.
      */
     private boolean satisfiedDependencies(FactoryState factory) {
-      for (Class<?> moduleOrFeature : factory.requires()) {
-        FactoryList factories = providesMap.get(moduleOrFeature.getTypeName());
-        if (factories == null || !factories.allPushed()) {
+      for (Class<?> dependency : factory.requires()) {
+        if (notProvided(dependency.getTypeName())) {
+          return false;
+        }
+      }
+      for (Class<?> dependency : factory.requiresPackages()) {
+        if (notProvided(dependency.getTypeName())) {
           return false;
         }
       }
@@ -349,6 +356,27 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
     Class<?>[] requires() {
       return factory.requires();
     }
+
+    Class<?>[] requiresPackages() {
+      return factory.requiresPackages();
+    }
+
+    @Override
+    public String toString() {
+      return factory.getClass().getTypeName();
+    }
+
+    boolean isRequiresEmpty() {
+      return isEmpty(factory.requires()) && isEmpty(factory.requiresPackages());
+    }
+
+    boolean isProvidesEmpty() {
+      return isEmpty(factory.provides());
+    }
+
+    private boolean isEmpty(@Nullable Class<?>[] values) {
+      return values == null || values.length == 0;
+    }
   }
 
   /**
diff --git a/inject/src/main/java/io/avaje/inject/InjectModule.java b/inject/src/main/java/io/avaje/inject/InjectModule.java
index c37d3668..25a20b25 100644
--- a/inject/src/main/java/io/avaje/inject/InjectModule.java
+++ b/inject/src/main/java/io/avaje/inject/InjectModule.java
@@ -60,6 +60,17 @@ public @interface InjectModule {
    */
   Class<?>[] requires() default {};
 
+  /**
+   * Dependencies in these packages are expected to be provided by other modules.
+   * <p>
+   * Instead of listing each and every dependency in {@code requires} we can use this to specify
+   * that any required dependency that is in these packages is expected to be provided by another module.
+   * <p>
+   * Use this rather than {@code requires} when there are lots of required dependencies, and we don't
+   * want to list each one in {@code requires} and {@code provides}.
+   */
+  Class<?>[] requiresPackages() default {};
+
   /**
    * Internal use only - identifies the custom scope annotation associated to this module.
    * <p>
diff --git a/inject/src/main/java/io/avaje/inject/spi/Module.java b/inject/src/main/java/io/avaje/inject/spi/Module.java
index 4bbf470b..dec3c0e4 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Module.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Module.java
@@ -5,15 +5,20 @@ package io.avaje.inject.spi;
  */
 public interface Module {
 
+  /**
+   * Return the set of types this module explicitly provides to other modules.
+   */
+  Class<?>[] provides();
+
   /**
    * Return the types this module needs to be provided externally or via other modules.
    */
   Class<?>[] requires();
 
   /**
-   * Return the set of types this module explicitly provides to other modules.
+   * Return the packages this module needs to be provided via other modules.
    */
-  Class<?>[] provides();
+  Class<?>[] requiresPackages();
 
   /**
    * Return public classes of the beans that would be registered by this module.
diff --git a/pom.xml b/pom.xml
index eaa62f6f..6cd3258d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
 
   <groupId>io.avaje</groupId>
   <artifactId>avaje-inject-parent</artifactId>
-  <version>8.0</version>
+  <version>8.1.RC1</version>
   <packaging>pom</packaging>
   <name>avaje inject parent</name>
   <description>parent pom for avaje inject library</description>


Test patch:
diff --git a/blackbox-test-inject/pom.xml b/blackbox-test-inject/pom.xml
index 6ac02c88..9812db97 100644
--- a/blackbox-test-inject/pom.xml
+++ b/blackbox-test-inject/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <artifactId>avaje-inject-parent</artifactId>
     <groupId>io.avaje</groupId>
-    <version>8.0</version>
+    <version>8.1.RC1</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
diff --git a/inject-test/pom.xml b/inject-test/pom.xml
index eb884165..4d26f450 100644
--- a/inject-test/pom.xml
+++ b/inject-test/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>8.0</version>
+    <version>8.1.RC1</version>
   </parent>
 
   <artifactId>avaje-inject-test</artifactId>
@@ -124,7 +124,7 @@
             <path>
               <groupId>io.avaje</groupId>
               <artifactId>avaje-inject-generator</artifactId>
-              <version>8.0</version>
+              <version>8.1.RC1</version>
             </path>
           </annotationProcessorPaths>
         </configuration>
diff --git a/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java b/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java
index b1993abd..28f722cb 100644
--- a/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java
+++ b/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java
@@ -35,6 +35,11 @@ class BeanScopeBuilderAddTest {
       return new Class[0];
     }
 
+    @Override
+    public Class<?>[] requiresPackages() {
+      return new Class[0];
+    }
+
     @Override
     public Class<?>[] provides() {
       return new Class[0];
diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
index 695371b8..c1abbd61 100644
--- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
@@ -8,6 +8,7 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class BeanScopeBuilderTest {
 
@@ -85,14 +86,68 @@ public class BeanScopeBuilderTest {
     assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two");
   }
 
+  @Test
+  public void name_requiresPackage() {
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true);
+    factoryOrder.add(bc("1", null, new Class[0], of(Mod3.class)));
+    factoryOrder.add(bc("2", null, new Class[0], of(Mod4.class)));
+    factoryOrder.add(bc("3", of(Mod3.class), new Class[0], of(Mod4.class)));
+    factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
+
+    factoryOrder.orderFactories();
+
+    assertThat(names(factoryOrder.factories())).containsExactly("4", "2", "3", "1");
+  }
+
+  @Test
+  public void name_requiresPackage_mixed() {
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true);
+    factoryOrder.add(bc("1", null, new Class[0], of(Mod3.class)));
+    factoryOrder.add(bc("2", null, of(Mod4.class), new Class[0]));
+    factoryOrder.add(bc("3", of(Mod3.class), new Class[0], of(Mod4.class)));
+    factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
+
+    factoryOrder.orderFactories();
+
+    assertThat(names(factoryOrder.factories())).containsExactly("4", "2", "3", "1");
+  }
+
+  @Test
+  public void missingRequiresPackage_expect_unsatisfiedRequiresPackages() {
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), false);
+    factoryOrder.add(bc("1", null, new Class[0], of(Mod3.class)));
+    factoryOrder.add(bc("2", null, of(Mod4.class), new Class[0]));
+    factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
+
+    assertThatThrownBy(factoryOrder::orderFactories)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessageContaining("has unsatisfied requiresPackages [io.avaje.inject.BeanScopeBuilderTest$Mod3] ");
+  }
+
+  @Test
+  public void missingRequires_expect_unsatisfiedRequires() {
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), false);
+    factoryOrder.add(bc("1", null, of(Mod3.class), new Class[0]));
+    factoryOrder.add(bc("2", null, of(Mod4.class), new Class[0]));
+    factoryOrder.add(bc("4", of(Mod4.class), new Class[0]));
+
+    assertThatThrownBy(factoryOrder::orderFactories)
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessageContaining("has unsatisfied requires [io.avaje.inject.BeanScopeBuilderTest$Mod3] ");
+  }
+
   private List<String> names(List<Module> factories) {
     return factories.stream()
       .map(Module::toString)
       .collect(Collectors.toList());
   }
 
-  private TDBeanScope bc(String name, Class<?>[] provides, Class<?>[] dependsOn) {
-    return new TDBeanScope(name, provides, dependsOn);
+  private TDBeanScope bc(String name, Class<?>[] provides, Class<?>[] requires) {
+    return bc(name, provides, requires, new Class[0]);
+  }
+
+  private TDBeanScope bc(String name, Class<?>[] provides, Class<?>[] requires, Class<?>[] requiresPkg) {
+    return new TDBeanScope(name, provides, requires, requiresPkg);
   }
 
   private static class TDBeanScope implements Module {
@@ -100,11 +155,13 @@ public class BeanScopeBuilderTest {
     final String name;
     final Class<?>[] provides;
     final Class<?>[] requires;
+    final Class<?>[] requiresPackages;
 
-    private TDBeanScope(String name, Class<?>[] provides, Class<?>[] requires) {
+    private TDBeanScope(String name, Class<?>[] provides, Class<?>[] requires, Class<?>[] requiresPackages) {
       this.name = name;
       this.provides = provides;
       this.requires = requires;
+      this.requiresPackages = requiresPackages;
     }
 
     @Override
@@ -127,6 +184,11 @@ public class BeanScopeBuilderTest {
       return requires;
     }
 
+    @Override
+    public Class<?>[] requiresPackages() {
+      return requiresPackages;
+    }
+
     @Override
     public void build(Builder parent) {