001package io.avaje.inject;
002
003import java.lang.reflect.Type;
004import java.util.function.Consumer;
005import java.util.function.Supplier;
006
007import org.jspecify.annotations.Nullable;
008
009import io.avaje.inject.spi.AvajeModule;
010import io.avaje.inject.spi.ConfigPropertyPlugin;
011
012/**
013 * Build a bean scope with options for shutdown hook and supplying external dependencies.
014 * <p>
015 * We can provide external dependencies that are then used in wiring the components.
016 * </p>
017 *
018 * <pre>{@code
019 *
020 *   // external dependencies
021 *   Pump pump = ...
022 *
023 *   BeanScope scope = BeanScope.builder()
024 *     .bean(pump)
025 *     .build();
026 *
027 *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
028 *   coffeeMaker.makeIt();
029 *
030 * }</pre>
031 */
032public interface BeanScopeBuilder {
033
034  /**
035   * Create the bean scope registering a shutdown hook (defaults to false, no shutdown hook).
036   * <p>
037   * With {@code withShutdownHook(true)} a shutdown hook will be registered with the Runtime
038   * and executed when the JVM initiates a shutdown. This then will run the {@code preDestroy}
039   * lifecycle methods.
040   * </p>
041   * <pre>{@code
042   *
043   *   // automatically closed via try with resources
044   *
045   *   BeanScope scope = BeanScope.builder()
046   *     .shutdownHook(true)
047   *     .build());
048   *
049   *   // on JVM shutdown the preDestroy lifecycle methods are executed
050   *
051   * }</pre>
052   *
053   * @return This BeanScopeBuilder
054   */
055  BeanScopeBuilder shutdownHook(boolean shutdownHook);
056
057  /**
058   * Specify the modules to include in dependency injection.
059   * <p>
060   * Only beans related to the module are included in the BeanScope that is built.
061   * <p>
062   * When we do not explicitly specify modules then all modules that are not "custom scoped"
063   * are found and used via service loading.
064   *
065   * <pre>{@code
066   *
067   *   BeanScope scope = BeanScope.builder()
068   *     .modules(new CustomModule())
069   *     .build());
070   *
071   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
072   *   coffeeMaker.makeIt();
073   *
074   * }</pre>
075   *
076   * @param modules The modules that we want to include in dependency injection.
077   * @return This BeanScopeBuilder
078   */
079  BeanScopeBuilder modules(AvajeModule... modules);
080
081  /**
082   * Set the ConfigPropertyPlugin used for this scope. This is serviceloaded automatically of not set
083   *
084   * @param propertyPlugin The plugin for conditions based on properties
085   */
086  void configPlugin(ConfigPropertyPlugin propertyPlugin);
087
088  /**
089   * Return the ConfigPropertyPlugin used for this scope. This is useful for plugins that want to use
090   * the scopes wiring properties.
091   */
092  ConfigPropertyPlugin configPlugin();
093
094  /**
095   * Supply a bean to the scope that will be used instead of any similar bean in the scope.
096   *
097   * <p>This is typically expected to be used in tests and the bean supplied is typically a test
098   * double or mock.
099   *
100   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
101   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
102   * {@code @Nullable} annotation where the bean is wired.
103   *
104   * <pre>{@code
105   * // external dependencies
106   * Pump pump = ...
107   * Grinder grinder = ...
108   *
109   * BeanScope scope = BeanScope.builder()
110   *   .beans(pump, grinder)
111   *   .build();
112   *
113   * CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
114   * coffeeMaker.makeIt();
115   *
116   * }</pre>
117   *
118   * @param beans Externally provided beans used when injecting a dependency for the bean or the
119   *              interface(s) it implements
120   * @return This BeanScopeBuilder
121   */
122  BeanScopeBuilder beans(Object... beans);
123
124  /**
125   * Add a supplied bean instance with the given injection type (typically an interface type).
126   *
127   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
128   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
129   * {@code @Nullable} annotation where the bean is wired.
130   *
131   * <pre>{@code
132   * Pump externalDependency = ...
133   *
134   * try (BeanScope scope = BeanScope.builder()
135   *   .bean(Pump.class, externalDependency)
136   *   .build()) {
137   *
138   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
139   *   coffeeMaker.makeIt();
140   *
141   *   Pump pump = scope.get(Pump.class);
142   *   assertThat(pump).isSameAs(externalDependency);
143   * }
144   *
145   * }</pre>
146   *
147   * @param type The dependency injection type this bean is target for
148   * @param bean The supplied bean instance to use for injection
149   */
150  <D> BeanScopeBuilder bean(Class<D> type, D bean);
151
152  /**
153   * Add a supplied bean instance with the given name and injection type.
154   *
155   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
156   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
157   * {@code @Nullable} annotation where the bean is wired.
158   *
159   * @param name The name qualifier
160   * @param type The dependency injection type this bean is target for
161   * @param bean The supplied bean instance to use for injection
162   */
163  <D> BeanScopeBuilder bean(String name, Class<D> type, D bean);
164
165  /**
166   * Add a supplied bean instance with the given name and generic type.
167   *
168   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
169   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
170   * {@code @Nullable} annotation where the bean is wired.
171   *
172   * @param name The name qualifier
173   * @param type The dependency injection type this bean is target for
174   * @param bean The supplied bean instance to use for injection
175   */
176  <D> BeanScopeBuilder bean(String name, Type type, D bean);
177
178  /**
179   * Add a supplied bean instance with a generic type.
180   *
181   * <p>If using this to provide a missing bean into the scope, Avaje will fail compilation unless
182   * it detects an {@code @InjectModule(requires)} including the missing class, or it detects a
183   * {@code @Nullable} annotation where the bean is wired.
184   *
185   * @param type The dependency injection type this bean is target for
186   * @param bean The supplied bean instance to use for injection
187   */
188  <D> BeanScopeBuilder bean(Type type, D bean);
189
190  /**
191   * Set the explicit profiles to use when building the scope.
192   *
193   * <p>If profiles are not set explicitly here they are read from the properties plugin.
194   */
195  BeanScopeBuilder profiles(String... profiles);
196
197  /**
198   * Add a supplied bean provider that acts as a default fallback for a dependency.
199   * <p>
200   * This provider is only called if nothing else provides the dependency. It effectively
201   * uses `@Secondary` priority.
202   *
203   * @param type     The type of the dependency
204   * @param provider The provider of the dependency.
205   */
206  default <D> BeanScopeBuilder provideDefault(Type type, Supplier<D> provider) {
207    return provideDefault(null, type, provider);
208  }
209
210  /**
211   * Add a supplied bean provider that acts as a default fallback for a dependency.
212   * <p>
213   * This provider is only called if nothing else provides the dependency. It effectively
214   * uses `@Secondary` priority.
215   *
216   * @param name     The name qualifier
217   * @param type     The type of the dependency
218   * @param provider The provider of the dependency.
219   */
220  <D> BeanScopeBuilder provideDefault(@Nullable String name, Type type, Supplier<D> provider);
221
222  /**
223   * Adds hooks that will execute after this scope is built.
224   */
225  BeanScopeBuilder addPostConstruct(Runnable postConstructHook);
226
227  /**
228   * Adds hook that will execute after this scope is built.
229   */
230  BeanScopeBuilder addPostConstruct(Consumer<BeanScope> postConstructHook);
231
232  /**
233   * Add hook that will execute before this scope is destroyed.
234   */
235  BeanScopeBuilder addPreDestroy(AutoCloseable preDestroyHook);
236
237  /**
238   * Add hook with a priority that will execute before this scope is destroyed.
239   * <p>
240   * Specify the priority of the destroy method to control its execution
241   * order relative to other destroy methods.
242   * <p>
243   * Low values for priority execute earlier than high values. All destroy methods
244   * without any explicit priority are given a value of 1000.
245   */
246  BeanScopeBuilder addPreDestroy(AutoCloseable preDestroyHook, int priority);
247
248  /**
249   * Set the ClassLoader to use when loading modules.
250   *
251   * @param classLoader The ClassLoader to use
252   */
253  BeanScopeBuilder classLoader(ClassLoader classLoader);
254
255  /**
256   * Use the given BeanScope as the parent. This becomes an additional
257   * source of beans that can be wired and accessed in this scope.
258   *
259   * @param parent The BeanScope that acts as the parent
260   */
261  BeanScopeBuilder parent(BeanScope parent);
262
263  /**
264   * Use the given BeanScope as the parent additionally specifying if beans
265   * added will effectively override beans that exist in the parent scope.
266   * <p>
267   * By default, child scopes will override a bean that exists in a parent scope.
268   * For testing purposes, parentOverride=false is used such that bean provided
269   * in parent test scopes are used (unless we mock() or spy() them).
270   * <p>
271   * See TestBeanScope in avaje-inject-test which has helper methods to build
272   * BeanScopes for testing with the "Global test scope" as a parent scope.
273   *
274   * @param parent         The BeanScope that acts as the parent
275   * @param parentOverride When false do not add beans that already exist on the parent.
276   *                       When true add beans regardless of whether they exist in the parent scope.
277   */
278  BeanScopeBuilder parent(BeanScope parent, boolean parentOverride);
279
280  /**
281   * Extend the builder to support testing using mockito with
282   * <code>withMock()</code> and <code>withSpy()</code> methods.
283   *
284   * @return The builder with extra testing support for mockito mocks and spies
285   */
286  BeanScopeBuilder.ForTesting forTesting();
287
288  /**
289   * Build and return the bean scope.
290   * <p>
291   * The BeanScope is effectively immutable in that all components are created
292   * and all PostConstruct lifecycle methods have been invoked.
293   * <p>
294   * The beanScope effectively contains eager singletons.
295   *
296   * @return The BeanScope
297   */
298  BeanScope build();
299
300  /**
301   * Extends the building with testing specific support for mocks and spies.
302   */
303  interface ForTesting extends BeanScopeBuilder {
304
305    /**
306     * Use a mockito mock when injecting this bean type.
307     *
308     * <pre>{@code
309     *
310     *   try (BeanScope scope = BeanScope.builder()
311     *     .forTesting()
312     *     .mock(Pump.class)
313     *     .mock(Grinder.class)
314     *     .build()) {
315     *
316     *
317     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
318     *     coffeeMaker.makeIt();
319     *
320     *     // this is a mockito mock
321     *     Grinder grinder = scope.get(Grinder.class);
322     *     verify(grinder).grindBeans();
323     *   }
324     *
325     * }</pre>
326     */
327    BeanScopeBuilder.ForTesting mock(Type type);
328
329    /**
330     * Register as a Mockito mock with a qualifier name.
331     *
332     * <pre>{@code
333     *
334     *   try (BeanScope scope = BeanScope.builder()
335     *     .forTesting()
336     *     .mock(Store.class, "red")
337     *     .mock(Store.class, "blue")
338     *     .build()) {
339     *
340     *     ...
341     *   }
342     *
343     * }</pre>
344     */
345    BeanScopeBuilder.ForTesting mock(Type type, String name);
346
347    /**
348     * Use a mockito mock when injecting this bean type additionally
349     * running setup on the mock instance.
350     *
351     * <pre>{@code
352     *
353     *   try (BeanScope scope = BeanScope.builder()
354     *     .forTesting()
355     *     .mock(Pump.class)
356     *     .mock(Grinder.class, grinder -> {
357     *
358     *       // setup the mock
359     *       when(grinder.grindBeans()).thenReturn("stub response");
360     *     })
361     *     .build()) {
362     *
363     *
364     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
365     *     coffeeMaker.makeIt();
366     *
367     *     // this is a mockito mock
368     *     Grinder grinder = scope.get(Grinder.class);
369     *     verify(grinder).grindBeans();
370     *   }
371     *
372     * }</pre>
373     */
374    <D> BeanScopeBuilder.ForTesting mock(Class<D> type, Consumer<D> consumer);
375
376    /**
377     * Use a mockito spy when injecting this bean type.
378     *
379     * <pre>{@code
380     *
381     *   try (BeanScope scope = BeanScope.builder()
382     *     .forTesting()
383     *     .spy(Pump.class)
384     *     .build()) {
385     *
386     *     // setup spy here ...
387     *     Pump pump = scope.get(Pump.class);
388     *     doNothing().when(pump).pumpSteam();
389     *
390     *     // act
391     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
392     *     coffeeMaker.makeIt();
393     *
394     *     verify(pump).pumpWater();
395     *     verify(pump).pumpSteam();
396     *   }
397     *
398     * }</pre>
399     */
400    BeanScopeBuilder.ForTesting spy(Type type);
401
402    /**
403     * Register a Mockito spy with a qualifier name.
404     *
405     * <pre>{@code
406     *
407     *   try (BeanScope scope = BeanScope.builder()
408     *     .forTesting()
409     *     .spy(Store.class, "red")
410     *     .spy(Store.class, "blue")
411     *     .build()) {
412     *
413     *     ...
414     *   }
415     *
416     * }</pre>
417     */
418    BeanScopeBuilder.ForTesting spy(Type type, String name);
419
420    /**
421     * Use a mockito spy when injecting this bean type additionally
422     * running setup on the spy instance.
423     *
424     * <pre>{@code
425     *
426     *   try (BeanScope scope = BeanScope.builder()
427     *     .forTesting()
428     *     .spy(Pump.class, pump -> {
429     *       // setup the spy
430     *       doNothing().when(pump).pumpWater();
431     *     })
432     *     .build()) {
433     *
434     *     // or setup here ...
435     *     Pump pump = scope.get(Pump.class);
436     *     doNothing().when(pump).pumpSteam();
437     *
438     *     // act
439     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
440     *     coffeeMaker.makeIt();
441     *
442     *     verify(pump).pumpWater();
443     *     verify(pump).pumpSteam();
444     *   }
445     *
446     * }</pre>
447     */
448    <D> BeanScopeBuilder.ForTesting spy(Class<D> type, Consumer<D> consumer);
449  }
450}