001package io.avaje.inject;
002
003import io.avaje.inject.spi.Module;
004import io.avaje.lang.NonNullApi;
005
006import java.lang.reflect.Type;
007import java.util.function.Consumer;
008
009/**
010 * Build a bean scope with options for shutdown hook and supplying external dependencies.
011 * <p>
012 * We can provide external dependencies that are then used in wiring the components.
013 * </p>
014 *
015 * <pre>{@code
016 *
017 *   // external dependencies
018 *   Pump pump = ...
019 *
020 *   BeanScope scope = BeanScope.newBuilder()
021 *     .withBean(pump)
022 *     .build();
023 *
024 *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
025 *   coffeeMaker.makeIt();
026 *
027 * }</pre>
028 */
029@NonNullApi
030public interface BeanScopeBuilder {
031
032  /**
033   * Create the bean scope registering a shutdown hook (defaults to false, no shutdown hook).
034   * <p>
035   * With {@code withShutdownHook(true)} a shutdown hook will be registered with the Runtime
036   * and executed when the JVM initiates a shutdown. This then will run the {@code preDestroy}
037   * lifecycle methods.
038   * </p>
039   * <pre>{@code
040   *
041   *   // automatically closed via try with resources
042   *
043   *   BeanScope scope = BeanScope.newBuilder()
044   *     .withShutdownHook(true)
045   *     .build());
046   *
047   *   // on JVM shutdown the preDestroy lifecycle methods are executed
048   *
049   * }</pre>
050   *
051   * @return This BeanScopeBuilder
052   */
053  BeanScopeBuilder withShutdownHook(boolean shutdownHook);
054
055  /**
056   * Specify the modules to include in dependency injection.
057   * <p>
058   * Only beans related to the module are included in the BeanScope that is built.
059   * <p>
060   * When we do not explicitly specify modules then all modules that are not "custom scoped"
061   * are found and used via service loading.
062   *
063   * <pre>{@code
064   *
065   *   BeanScope scope = BeanScope.newBuilder()
066   *     .withModules(new CustomModule())
067   *     .build());
068   *
069   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
070   *   coffeeMaker.makeIt();
071   *
072   * }</pre>
073   *
074   * @param modules The modules that we want to include in dependency injection.
075   * @return This BeanScopeBuilder
076   */
077  BeanScopeBuilder withModules(Module... modules);
078
079  /**
080   * Supply a bean to the scope that will be used instead of any
081   * similar bean in the scope.
082   * <p>
083   * This is typically expected to be used in tests and the bean
084   * supplied is typically a test double or mock.
085   * </p>
086   *
087   * <pre>{@code
088   *
089   *   // external dependencies
090   *   Pump pump = ...
091   *   Grinder grinder = ...
092   *
093   *   BeanScope scope = BeanScope.newBuilder()
094   *     .withBeans(pump, grinder)
095   *     .build();
096   *
097   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
098   *   coffeeMaker.makeIt();
099   *
100   * }</pre>
101   *
102   * @param beans Externally provided beans used when injecting a dependency
103   *              for the bean or the interface(s) it implements
104   * @return This BeanScopeBuilder
105   */
106  BeanScopeBuilder withBeans(Object... beans);
107
108  /**
109   * Add a supplied bean instance with the given injection type (typically an interface type).
110   *
111   * <pre>{@code
112   *
113   *   Pump externalDependency = ...
114   *
115   *   try (BeanScope scope = BeanScope.newBuilder()
116   *     .withBean(Pump.class, externalDependency)
117   *     .build()) {
118   *
119   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
120   *     coffeeMaker.makeIt();
121   *
122   *     Pump pump = scope.get(Pump.class);
123   *     assertThat(pump).isSameAs(externalDependency);
124   *   }
125   *
126   * }</pre>
127   *
128   * @param type The dependency injection type this bean is target for
129   * @param bean The supplied bean instance to use for injection
130   */
131  <D> BeanScopeBuilder withBean(Class<D> type, D bean);
132
133  /**
134   * Add a supplied bean instance with the given name and injection type.
135   *
136   * @param name The name qualifier
137   * @param type The dependency injection type this bean is target for
138   * @param bean The supplied bean instance to use for injection
139   */
140  <D> BeanScopeBuilder withBean(String name, Class<D> type, D bean);
141
142  /**
143   * Add a supplied bean instance with the given name and generic type.
144   *
145   * @param name The name qualifier
146   * @param type The dependency injection type this bean is target for
147   * @param bean The supplied bean instance to use for injection
148   */
149  <D> BeanScopeBuilder withBean(String name, Type type, D bean);
150
151  /**
152   * Add a supplied bean instance with a generic type.
153   *
154   * @param type The dependency injection type this bean is target for
155   * @param bean The supplied bean instance to use for injection
156   */
157  <D> BeanScopeBuilder withBean(Type type, D bean);
158
159  /**
160   * Use the given BeanScope as the parent. This becomes an additional
161   * source of beans that can be wired and accessed in this scope.
162   *
163   * @param parent The BeanScope that acts as the parent
164   */
165  BeanScopeBuilder withParent(BeanScope parent);
166
167  /**
168   * Use the given BeanScope as the parent additionally specifying if beans
169   * added will effectively override beans that exist in the parent scope.
170   *
171   * @param parent         The BeanScope that acts as the parent
172   * @param parentOverride When false do not add beans that already exist on the parent.
173   *                       When true add beans regardless of whether they exist in the parent scope.
174   */
175  BeanScopeBuilder withParent(BeanScope parent, boolean parentOverride);
176
177  /**
178   * Extend the builder to support testing using mockito with
179   * <code>withMock()</code> and <code>withSpy()</code> methods.
180   *
181   * @return The builder with extra testing support for mockito mocks and spies
182   */
183  BeanScopeBuilder.ForTesting forTesting();
184
185  /**
186   * Build and return the bean scope.
187   * <p>
188   * The BeanScope is effectively immutable in that all components are created
189   * and all PostConstruct lifecycle methods have been invoked.
190   * <p>
191   * The beanScope effectively contains eager singletons.
192   *
193   * @return The BeanScope
194   */
195  BeanScope build();
196
197  /**
198   * Extends the building with testing specific support for mocks and spies.
199   */
200  interface ForTesting extends BeanScopeBuilder {
201
202    /**
203     * Use a mockito mock when injecting this bean type.
204     *
205     * <pre>{@code
206     *
207     *   try (BeanScope scope = BeanScope.newBuilder()
208     *     .forTesting()
209     *     .withMock(Pump.class)
210     *     .withMock(Grinder.class)
211     *     .build()) {
212     *
213     *
214     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
215     *     coffeeMaker.makeIt();
216     *
217     *     // this is a mockito mock
218     *     Grinder grinder = scope.get(Grinder.class);
219     *     verify(grinder).grindBeans();
220     *   }
221     *
222     * }</pre>
223     */
224    BeanScopeBuilder.ForTesting withMock(Class<?> type);
225
226    /**
227     * Register as a Mockito mock with a qualifier name.
228     *
229     * <pre>{@code
230     *
231     *   try (BeanScope scope = BeanScope.newBuilder()
232     *     .forTesting()
233     *     .withMock(Store.class, "red")
234     *     .withMock(Store.class, "blue")
235     *     .build()) {
236     *
237     *     ...
238     *   }
239     *
240     * }</pre>
241     */
242    BeanScopeBuilder.ForTesting withMock(Class<?> type, String name);
243
244    /**
245     * Use a mockito mock when injecting this bean type additionally
246     * running setup on the mock instance.
247     *
248     * <pre>{@code
249     *
250     *   try (BeanScope scope = BeanScope.newBuilder()
251     *     .forTesting()
252     *     .withMock(Pump.class)
253     *     .withMock(Grinder.class, grinder -> {
254     *
255     *       // setup the mock
256     *       when(grinder.grindBeans()).thenReturn("stub response");
257     *     })
258     *     .build()) {
259     *
260     *
261     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
262     *     coffeeMaker.makeIt();
263     *
264     *     // this is a mockito mock
265     *     Grinder grinder = scope.get(Grinder.class);
266     *     verify(grinder).grindBeans();
267     *   }
268     *
269     * }</pre>
270     */
271    <D> BeanScopeBuilder.ForTesting withMock(Class<D> type, Consumer<D> consumer);
272
273    /**
274     * Use a mockito spy when injecting this bean type.
275     *
276     * <pre>{@code
277     *
278     *   try (BeanScope scope = BeanScope.newBuilder()
279     *     .forTesting()
280     *     .withSpy(Pump.class)
281     *     .build()) {
282     *
283     *     // setup spy here ...
284     *     Pump pump = scope.get(Pump.class);
285     *     doNothing().when(pump).pumpSteam();
286     *
287     *     // act
288     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
289     *     coffeeMaker.makeIt();
290     *
291     *     verify(pump).pumpWater();
292     *     verify(pump).pumpSteam();
293     *   }
294     *
295     * }</pre>
296     */
297    BeanScopeBuilder.ForTesting withSpy(Class<?> type);
298
299    /**
300     * Register a Mockito spy with a qualifier name.
301     *
302     * <pre>{@code
303     *
304     *   try (BeanScope scope = BeanScope.newBuilder()
305     *     .forTesting()
306     *     .withSpy(Store.class, "red")
307     *     .withSpy(Store.class, "blue")
308     *     .build()) {
309     *
310     *     ...
311     *   }
312     *
313     * }</pre>
314     */
315    BeanScopeBuilder.ForTesting withSpy(Class<?> type, String name);
316
317    /**
318     * Use a mockito spy when injecting this bean type additionally
319     * running setup on the spy instance.
320     *
321     * <pre>{@code
322     *
323     *   try (BeanScope scope = BeanScope.newBuilder()
324     *     .forTesting()
325     *     .withSpy(Pump.class, pump -> {
326     *       // setup the spy
327     *       doNothing().when(pump).pumpWater();
328     *     })
329     *     .build()) {
330     *
331     *     // or setup here ...
332     *     Pump pump = scope.get(Pump.class);
333     *     doNothing().when(pump).pumpSteam();
334     *
335     *     // act
336     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
337     *     coffeeMaker.makeIt();
338     *
339     *     verify(pump).pumpWater();
340     *     verify(pump).pumpSteam();
341     *   }
342     *
343     * }</pre>
344     */
345    <D> BeanScopeBuilder.ForTesting withSpy(Class<D> type, Consumer<D> consumer);
346
347  }
348}