diff --git a/CHANGELOG.md b/CHANGELOG.md index b7edd7d05..fa002fb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Framework integration points are now instance-scoped (#198), and the Ktor integr - Shared Spring Boot auto-configurations in storm-spring (`st.orm.spring.boot`): `StormTransactionAutoConfiguration` (Spring-aware provider beans), `StormValidationAutoConfiguration` (startup schema validation), and the `storm.*` configuration properties (`StormProperties`), used by both starters. Configuration keys are unchanged. - SQL failures raised by Storm translate to Spring's `DataAccessException` hierarchy in Spring applications: `SpringExceptionMapper` (storm-spring) translates on vendor error codes with `SQLException` subclass and SQL state fallback, auto-configured by both starters (`storm.exception-translation.enabled=false` to disable) and applied by the `SpringOrmTemplate.of` / `springOrmTemplate` compositions. Failures without a `SQLException` cause keep Storm's own exceptions. - The Spring Boot starters bind query observations automatically: with an `ObservationRegistry` bean present (Actuator provides one), every query executed by the auto-configured template reports as a `storm.query` Micrometer Observation via the shared `StormObservationAutoConfiguration`. The starters ship storm-micrometer; override the convention with an `ObservationConvention` bean, replace the binding with a `QueryObserver` bean, or disable via `management.observations.enable.storm.query=false`. +- `@EnableStormRepositories(basePackages = ...)` (storm-spring, usable from both language stacks): switches on repository scanning in plain Spring applications, mirroring `@EnableJpaRepositories`; without packages, the annotated class's package is scanned. In Spring Boot the annotation doubles as the explicit override: the auto-configured scanning backs off. The `RepositoryBeanFactoryPostProcessor` adapters also gain configuring constructors (base packages, template bean name, prefix), so multi-template applications define one bean per repository set instead of one subclass. ### Changed diff --git a/docs/spring-integration.md b/docs/spring-integration.md index a5ebd44e6..06f459b11 100644 --- a/docs/spring-integration.md +++ b/docs/spring-integration.md @@ -145,14 +145,24 @@ fun processUsers() { ### Repository Injection -Storm repositories are interfaces with default method implementations. Spring cannot discover them automatically because they are not annotated with `@Component` or `@Repository`. The `RepositoryBeanFactoryPostProcessor` (in `st.orm.spring.kotlin` since 1.13) scans specified packages for interfaces that extend `EntityRepository` or `ProjectionRepository` and registers them as Spring beans. This makes them available for constructor injection like any other Spring-managed dependency. +Storm repositories are interfaces with default method implementations. Spring cannot discover them automatically because they are not annotated with `@Component` or `@Repository`. Enable scanning with `@EnableStormRepositories`, mirroring annotations like `@EnableJpaRepositories`: the given packages are scanned for interfaces that extend `EntityRepository` or `ProjectionRepository`, and each is registered as a Spring bean, available for constructor injection like any other Spring-managed dependency. ```kotlin @Configuration -class AcmeRepositoryBeanFactoryPostProcessor : RepositoryBeanFactoryPostProcessor() { +@EnableStormRepositories(basePackages = ["com.acme.repository"]) +class AcmeConfiguration +``` + +Without explicit packages, the package of the annotated class is scanned. For full control, or for multiple repository sets bound to different templates, define `RepositoryBeanFactoryPostProcessor` beans (from `st.orm.spring.kotlin`) instead: + +```kotlin +@Configuration +class AcmeRepositoryConfiguration { - override fun getRepositoryBasePackages(): Array = - arrayOf("com.acme.repository") + @Bean + fun acmeRepositories() = RepositoryBeanFactoryPostProcessor( + basePackages = arrayOf("com.acme.repository"), + ) } ``` @@ -220,15 +230,25 @@ A template created with `ORMTemplate.of(dataSource)` works too, but manages its ### Repository Injection -Register a `RepositoryBeanFactoryPostProcessor` that scans your repository packages. This works identically to the Kotlin version: Storm discovers interfaces extending `EntityRepository` or `ProjectionRepository` and registers them as Spring beans. +Enable repository scanning with `@EnableStormRepositories`. This works identically to the Kotlin version: Storm discovers interfaces extending `EntityRepository` or `ProjectionRepository` and registers them as Spring beans. + +```java +@Configuration +@EnableStormRepositories(basePackages = "com.acme.repository") +public class AcmeConfiguration { +} +``` + +For full control, or for multiple repository sets bound to different templates, define `RepositoryBeanFactoryPostProcessor` beans instead: ```java @Configuration -public class AcmeRepositoryBeanFactoryPostProcessor extends RepositoryBeanFactoryPostProcessor { +public class AcmeRepositoryConfiguration { - @Override - public String[] getRepositoryBasePackages() { - return new String[] { "com.acme.repository" }; + @Bean + public RepositoryBeanFactoryPostProcessor acmeRepositories() { + return new RepositoryBeanFactoryPostProcessor( + new String[] { "com.acme.repository" }, null, ""); } } ``` @@ -537,11 +557,8 @@ class StormConfig(private val dataSource: DataSource) { ```kotlin @Configuration -class MyRepositoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - - override fun getRepositoryBasePackages(): Array = - arrayOf("com.myapp.repository", "com.myapp.other") -} +@EnableStormRepositories(basePackages = ["com.myapp.repository", "com.myapp.other"]) +class RepositoryConfiguration ``` ### Minimal Spring Boot Setup (without Starter) @@ -564,11 +581,8 @@ class StormConfig { } @Configuration -class MyRepositoryBeanFactoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - - override fun getRepositoryBasePackages(): Array = - arrayOf("com.myapp.repository") -} +@EnableStormRepositories(basePackages = ["com.myapp.repository"]) +class RepositoryConfiguration ``` This gives you: @@ -637,14 +651,14 @@ Repositories bind to a specific template through a per-domain post-processor. Th ```kotlin @Configuration -class BillingRepositoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - - override fun getOrmTemplateBeanName(): String = "billingTemplate" +class BillingRepositoryConfiguration { - override fun getRepositoryPrefix(): String = "billing" - - override fun getRepositoryBasePackages(): Array = - arrayOf("com.myapp.billing.repository") + @Bean + fun billingRepositories() = RepositoryBeanFactoryPostProcessor( + basePackages = arrayOf("com.myapp.billing.repository"), + ormTemplateBeanName = "billingTemplate", + repositoryPrefix = "billing", + ) } ``` diff --git a/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt b/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt index 330751f9c..f40e7298f 100644 --- a/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt +++ b/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt @@ -27,11 +27,22 @@ import st.orm.template.ORMTemplate * BeanFactoryPostProcessor that scans base packages for Repository interfaces and registers them as beans. * * The scanning and registration engine lives in [AbstractRepositoryBeanFactoryPostProcessor]; this class binds - * it to the Kotlin API's repository types. Subclasses override the engine's methods, e.g. - * `override fun getRepositoryBasePackages(): Array`. + * it to the Kotlin API's repository types. Configure through the constructor (one bean per domain in + * multi-template applications), by subclassing (`override fun getRepositoryBasePackages(): Array`), + * or with `@EnableStormRepositories`. */ @Component -open class RepositoryBeanFactoryPostProcessor : AbstractRepositoryBeanFactoryPostProcessor() { +open class RepositoryBeanFactoryPostProcessor( + private val basePackages: Array = emptyArray(), + private val ormTemplateBeanName: String? = null, + private val repositoryPrefix: String = "", +) : AbstractRepositoryBeanFactoryPostProcessor() { + + override fun getRepositoryBasePackages(): Array = if (basePackages.isNotEmpty()) basePackages.copyOf() else super.getRepositoryBasePackages() + + override fun getOrmTemplateBeanName(): String? = ormTemplateBeanName ?: super.getOrmTemplateBeanName() + + override fun getRepositoryPrefix(): String = repositoryPrefix.ifEmpty { super.getRepositoryPrefix() } override fun getRepositoryType(): Class<*> = Repository::class.java diff --git a/storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt b/storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt new file mode 100644 index 000000000..d9e34de99 --- /dev/null +++ b/storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt @@ -0,0 +1,57 @@ +package st.orm.spring + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import org.junit.jupiter.api.Test +import org.springframework.boot.jdbc.DataSourceBuilder +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.ClassPathResource +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator +import st.orm.spring.repository.VisitRepository +import st.orm.template.ORMTemplate +import javax.sql.DataSource + +/** + * Verifies that [EnableStormRepositories] binds repositories through the Kotlin adapter when + * storm-kotlin-spring is on the classpath. + */ +class EnableStormRepositoriesTest { + + @Configuration + open class DatabaseConfiguration { + @Bean + open fun dataSource(): DataSource { + val dataSource = DataSourceBuilder.create() + .url("jdbc:h2:mem:enablerepositoriestestkt;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false") + .username("sa") + .password("") + .driverClassName("org.h2.Driver") + .build() + ResourceDatabasePopulator(ClassPathResource("data.sql")).execute(dataSource) + return dataSource + } + + @Bean + open fun ormTemplate(dataSource: DataSource): ORMTemplate = ORMTemplate.of(dataSource) + } + + @Configuration + @EnableStormRepositories(basePackages = ["st.orm.spring.repository"], ormTemplateBeanName = "ormTemplate") + open class RepositoriesConfiguration + + @Test + fun `annotation registers repositories through the Kotlin adapter`() { + AnnotationConfigApplicationContext( + DatabaseConfiguration::class.java, + RepositoriesConfiguration::class.java, + ).use { context -> + // The registrar must pick the Kotlin adapter: repositories bind through the KClass API. + context.getBean("stormRepositoriesPostProcessor") + .shouldBeInstanceOf() + val visitRepository = context.getBean(VisitRepository::class.java) + visitRepository.count() shouldBe 14 + } + } +} diff --git a/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java b/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java index e5f12f3c0..b66427b56 100644 --- a/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java +++ b/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java @@ -548,6 +548,27 @@ public io.micrometer.observation.tck.TestObservationRegistry observationRegistry } } + @Test + void enableStormRepositoriesBacksOffAutoConfiguredScanning() { + // @EnableStormRepositories doubles as the explicit override in Boot: its registrar-provided + // post-processor makes the auto-configured one back off. + contextRunner + .withPropertyValues( + "spring.datasource.url=jdbc:h2:mem:enableAnnotationTest;DB_CLOSE_DELAY=-1", + "spring.datasource.driver-class-name=org.h2.Driver" + ) + .withUserConfiguration(EnableStormRepositoriesConfig.class) + .run(context -> { + assertThat(context).doesNotHaveBean(AutoConfiguredRepositoryBeanFactoryPostProcessor.class); + assertThat(context).hasBean("stormRepositoriesPostProcessor"); + }); + } + + @Configuration + @st.orm.spring.EnableStormRepositories(basePackages = "com.example.repository") + static class EnableStormRepositoriesConfig { + } + @Configuration static class CustomExceptionMapperConfig { @Bean diff --git a/storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java b/storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java new file mode 100644 index 000000000..2375d352d --- /dev/null +++ b/storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 - 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package st.orm.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.annotation.Import; +import st.orm.spring.impl.StormRepositoriesRegistrar; + +/** + * Enables Storm repository beans, mirroring annotations like {@code @EnableJpaRepositories}. + * + *

Scans the configured base packages for repository interfaces and registers each as a Spring bean, + * available for constructor injection. Without explicit packages, the package of the annotated class is + * scanned. Works with both language stacks: the repositories are bound through the Kotlin API when + * storm-kotlin-spring is on the classpath, and through the Java API otherwise.

+ * + * {@snippet lang = java: + * @Configuration + * @EnableStormRepositories(basePackages = "com.acme.repository") + * public class AcmeConfiguration { + * } + * } + * + *

In Spring Boot, repositories are discovered automatically under the application's base package; this + * annotation doubles as the explicit override there — when present, the starter's auto-configured scanning + * backs off. For multiple repository sets bound to different templates, define one + * {@link RepositoryBeanFactoryPostProcessor} bean per set using its configuring constructor instead.

+ * + * @since 1.13 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(StormRepositoriesRegistrar.class) +public @interface EnableStormRepositories { + + /** + * Base packages to scan for repository interfaces. Empty means the package of the annotated class. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()}: the packages of the given classes are scanned. + */ + Class[] basePackageClasses() default {}; + + /** + * The {@code ORMTemplate} bean the repositories bind to. Empty means the primary template. + */ + String ormTemplateBeanName() default ""; + + /** + * Prefix for the registered repository bean names. Empty for none. + */ + String repositoryPrefix() default ""; +} diff --git a/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java b/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java index 3599904fd..748caa3b9 100644 --- a/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java +++ b/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java @@ -16,6 +16,7 @@ package st.orm.spring; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import java.util.Set; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.stereotype.Component; @@ -33,6 +34,49 @@ @Component public class RepositoryBeanFactoryPostProcessor extends AbstractRepositoryBeanFactoryPostProcessor { + private final String[] basePackages; + private final String ormTemplateBeanName; + private final String repositoryPrefix; + + /** + * Creates a post-processor configured through overrides; subclasses override + * {@link #getRepositoryBasePackages()} and friends. + */ + public RepositoryBeanFactoryPostProcessor() { + this(new String[0], null, ""); + } + + /** + * Creates a fully configured post-processor, without subclassing. + * + * @param basePackages the base packages to scan for repository interfaces. + * @param ormTemplateBeanName the ORMTemplate bean repositories bind to, or {@code null} for the primary. + * @param repositoryPrefix prefix for the registered repository bean names; empty for none. + * @since 1.13 + */ + public RepositoryBeanFactoryPostProcessor(@Nonnull String[] basePackages, + @Nullable String ormTemplateBeanName, + @Nonnull String repositoryPrefix) { + this.basePackages = basePackages.clone(); + this.ormTemplateBeanName = ormTemplateBeanName; + this.repositoryPrefix = repositoryPrefix; + } + + @Override + public String[] getRepositoryBasePackages() { + return basePackages.length > 0 ? basePackages.clone() : super.getRepositoryBasePackages(); + } + + @Override + public String getOrmTemplateBeanName() { + return ormTemplateBeanName != null ? ormTemplateBeanName : super.getOrmTemplateBeanName(); + } + + @Override + protected String getRepositoryPrefix() { + return !repositoryPrefix.isEmpty() ? repositoryPrefix : super.getRepositoryPrefix(); + } + @Override protected Class getRepositoryType() { return Repository.class; diff --git a/storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java b/storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java new file mode 100644 index 000000000..1abc04d58 --- /dev/null +++ b/storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 - 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package st.orm.spring.impl; + +import jakarta.annotation.Nonnull; +import java.util.LinkedHashSet; +import java.util.Set; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; +import st.orm.spring.AbstractRepositoryBeanFactoryPostProcessor; +import st.orm.spring.EnableStormRepositories; +import st.orm.spring.RepositoryBeanFactoryPostProcessor; + +/** + * Registers the repository scanning post-processor for {@link EnableStormRepositories}. + * + *

The repositories are bound through the language stack present on the classpath: the Kotlin adapter + * when storm-kotlin-spring is available, the Java adapter otherwise (exactly one language stack exists per + * application, as the stacks share class names).

+ * + * @since 1.13 + */ +public class StormRepositoriesRegistrar implements ImportBeanDefinitionRegistrar { + + static final String BEAN_NAME = "stormRepositoriesPostProcessor"; + + private static final String KOTLIN_ADAPTER = "st.orm.spring.kotlin.RepositoryBeanFactoryPostProcessor"; + + @Override + public void registerBeanDefinitions(@Nonnull AnnotationMetadata importingClassMetadata, + @Nonnull BeanDefinitionRegistry registry) { + var attributes = AnnotationAttributes.fromMap( + importingClassMetadata.getAnnotationAttributes(EnableStormRepositories.class.getName())); + if (attributes == null) { + return; + } + Set basePackages = new LinkedHashSet<>(); + for (String basePackage : attributes.getStringArray("basePackages")) { + if (!basePackage.isBlank()) { + basePackages.add(basePackage); + } + } + for (Class basePackageClass : attributes.getClassArray("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(basePackageClass)); + } + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); + } + String ormTemplateBeanName = attributes.getString("ormTemplateBeanName"); + String repositoryPrefix = attributes.getString("repositoryPrefix"); + var beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(resolvePostProcessorType()) + .addConstructorArgValue(basePackages.toArray(String[]::new)) + .addConstructorArgValue(ormTemplateBeanName.isEmpty() ? null : ormTemplateBeanName) + .addConstructorArgValue(repositoryPrefix) + .getBeanDefinition(); + beanDefinition.setRole(org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(BEAN_NAME, beanDefinition); + } + + @SuppressWarnings("unchecked") + private Class resolvePostProcessorType() { + ClassLoader classLoader = StormRepositoriesRegistrar.class.getClassLoader(); + if (ClassUtils.isPresent(KOTLIN_ADAPTER, classLoader)) { + try { + return (Class) + ClassUtils.forName(KOTLIN_ADAPTER, classLoader); + } catch (ClassNotFoundException ignored) { + // Fall through to the Java adapter. + } + } + return RepositoryBeanFactoryPostProcessor.class; + } +} diff --git a/storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java b/storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java new file mode 100644 index 000000000..0966aabcb --- /dev/null +++ b/storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2024 - 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package st.orm.spring; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import st.orm.spring.repository.VisitRepository; +import st.orm.template.ORMTemplate; + +/** + * Verifies that {@link EnableStormRepositories} scans and registers repository beans, mirroring + * annotations like {@code @EnableJpaRepositories}. + */ +class EnableStormRepositoriesTest { + + @Configuration + static class DatabaseConfiguration { + @Bean + public DataSource dataSource() { + DataSource dataSource = DataSourceBuilder.create() + .url("jdbc:h2:mem:enablerepositoriestest;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false") + .username("sa") + .password("") + .driverClassName("org.h2.Driver") + .build(); + new ResourceDatabasePopulator(new ClassPathResource("data.sql")).execute(dataSource); + return dataSource; + } + + @Bean + public ORMTemplate ormTemplate(DataSource dataSource) { + return ORMTemplate.of(dataSource); + } + } + + @Configuration + @EnableStormRepositories(basePackages = "st.orm.spring.repository", ormTemplateBeanName = "ormTemplate") + static class ExplicitPackagesConfiguration { + } + + @Configuration + @EnableStormRepositories(basePackageClasses = VisitRepository.class, ormTemplateBeanName = "ormTemplate") + static class PackageClassesConfiguration { + } + + @Configuration + @EnableStormRepositories( + basePackages = "st.orm.spring.repository", + ormTemplateBeanName = "ormTemplate", + repositoryPrefix = "acme") + static class PrefixedConfiguration { + } + + @Test + void explicitBasePackagesRegisterRepositories() { + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, ExplicitPackagesConfiguration.class)) { + VisitRepository visitRepository = context.getBean(VisitRepository.class); + assertEquals(14, visitRepository.count()); + assertInstanceOf(RepositoryBeanFactoryPostProcessor.class, + context.getBean("stormRepositoriesPostProcessor")); + } + } + + @Test + void basePackageClassesRegisterRepositories() { + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, PackageClassesConfiguration.class)) { + assertTrue(context.containsBean("VisitRepository")); + } + } + + @Test + void repositoryPrefixNamesTheBeans() { + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, PrefixedConfiguration.class)) { + assertTrue(context.containsBean("acmeVisitRepository")); + VisitRepository visitRepository = context.getBean(VisitRepository.class); + assertEquals(14, visitRepository.count()); + } + } + + @Test + void withoutPackagesTheAnnotatedClassPackageIsScanned() { + // The test configuration lives in st.orm.spring, so the scan covers st.orm.spring.repository too. + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, DefaultPackageConfiguration.class)) { + assertTrue(context.containsBean("VisitRepository")); + } + } + + @Configuration + @EnableStormRepositories(ormTemplateBeanName = "ormTemplate") + static class DefaultPackageConfiguration { + } +}