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