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