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