15 Commits

Author SHA1 Message Date
hlaeja
3b023ad364 [RELEASE] - Bump version 2025-08-18 10:42:06 +00:00
hlaeja
7a8ded192c [RELEASE] - Release version: 0.4.0 2025-08-18 10:42:05 +00:00
370d8fb81d add kafka and postgres test container 2025-08-18 12:41:26 +02:00
ca85a7ac64 add kafka test container 2025-08-18 12:41:26 +02:00
2328e7ebe2 Rewrite postgres test container 2025-08-18 12:41:26 +02:00
hlaeja
4bc14b5340 [RELEASE] - Bump version 2025-07-29 17:51:47 +00:00
hlaeja
e7b67eea89 [RELEASE] - Release version: 0.3.0 2025-07-29 17:51:46 +00:00
3ffabf3011 update project 2025-07-29 19:51:15 +02:00
7f2609d0a5 add GitHub action
- update release in README.md
- add action run checks
- add action release
- remove release.sh
2025-07-29 19:51:15 +02:00
1fb647b733 update gradlew 2025-07-29 19:51:15 +02:00
bea0d78d75 [RELEASE] - bump version 2025-04-05 12:44:29 +02:00
b129528c35 [RELEASE] - release version: 0.2.0 2025-04-05 12:44:26 +02:00
6aad7e3d63 update postgres test container
- update README.md
- update PostgresContainer
  - add TestExecutionListeners
  - remove ExtendWith
- update PostgresInitializer
  - cleanup
  - use properties for script and container
  - add afterTestClass
  - add beforeTestClass
  - extend TestExecutionListener
- remove PostgresExtension
- add debug logging to PostgresExecutor
- add ContainerUtils
- add dependencies
- extract function from PostgresExtension to PostgresExecutor
2025-03-10 19:48:44 +01:00
902a2a2c0b add assertion to compare uuid against string representation of an uuid 2025-03-05 16:07:28 +01:00
be08c0c369 [RELEASE] - bump version 2025-02-07 17:05:29 +01:00
24 changed files with 300 additions and 225 deletions

View File

@@ -19,12 +19,14 @@ end_of_line = crlf
# noinspection EditorConfigKeyCorrectness
[*.{kt,kts}]
ij_kotlin_packages_to_use_import_on_demand = unset
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_allow_trailing_comma = true
ktlint_standard_import-ordering = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_packages_to_use_import_on_demand = unset
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_import-ordering = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_string-template-indent = disabled

12
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Release
on:
workflow_dispatch:
jobs:
release:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/release.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
with:
TYPE: artifact

12
.github/workflows/run-checks.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Pull request validation
on:
pull_request:
paths-ignore:
- '.github/**'
jobs:
validate:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/run-checks.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}

View File

@@ -2,22 +2,35 @@
In the forge of software development, where annotations ignite, A crucible of testing, common classes to excite. Each annotation examined, with attention to detail and might, Their effects on code behavior, tested through day and night. From mockk objects to test doubles, a toolkit to refine, Developers and testers, their skills to redefine. The Annotation Validator, a sentinel of code integrity true, A library of verification, where testing wisdom shines anew.
## Kafka and Postgres Test Container
`@KafkaPostgresTestContainer` Annotation for integration tests.
This combined `@KafkaTestContainer` and `@PostgresTestContainer`
## Kafka Test Container
`@KafkaTestContainer` Annotation for integration tests.
Initialize Kafka test container using test container default properties
## Postgres Test Container
`@PostgresContainer` Annotation for integration tests.
`@PostgresTestContainer` Annotation for integration tests.
Initialize Postgres test container using spring properties for R2DBC,
script located in `src/<test path>/resources/postgres` folder.
Initialize Postgres test container using test container default properties, script located in `src/<test path>/resources/postgres/` folder.
* `schema.sql` file containing all structure and functions when star.
* `data.sql` file containing all data added before all test.
* `reset.sql` file containing all to reset database after all test.
### Properties For Test Container
if file exist it will be loaded...
| file | required | info |
|---------------------|:--------:|----------------------------------------------------------------|
| postgres/schema.sql | &check; | Postgres init script containing all structure and functions |
| postgres/data.sql | | Postgres data script containing all data to populate database |
| postgres/reset.sql | | Postgres reset script containing all command to reset database |
## Releasing library
Run `release.sh` script from `master` branch.
Run release pipeline from `master` branch.
## Publishing library

View File

@@ -1,16 +1,18 @@
plugins {
alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.library)
alias(hlaeja.plugins.spring.boot)
alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot)
alias(hlaeja.plugins.library)
}
dependencies {
implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.test)
implementation(hlaeja.testcontainers.junit)
implementation(hlaeja.testcontainers.kafka)
implementation(hlaeja.testcontainers.postgresql)
testRuntimeOnly(hlaeja.junit.platform.launcher)

View File

@@ -1,3 +1,3 @@
kotlin.code.style=official
version=0.1.0
catalog=0.9.0
version=0.5.0-SNAPSHOT
catalog=0.12.0

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1,89 +0,0 @@
#!/bin/sh
### This should be a pipeline, but for this example let use this ###
check_active_branch() {
if [ "$(git rev-parse --abbrev-ref HEAD)" != "$1" ]; then
echo "Error: The current branch is not $1."
exit 1
fi
}
check_uncommitted_changes() {
if [ -n "$(git status --porcelain)" ]; then
echo "Error: There are uncommitted changes in the repository."
exit 1
fi
}
prepare_environment() {
git fetch origin
}
check_last_commit() {
last_commit_message=$(git log -1 --pretty=format:%s)
if [ "$last_commit_message" = "[RELEASE] - bump version" ]; then
echo "Warning: Nothing to release!!!"
exit 1
fi
}
check_differences() {
if ! git diff --quiet origin/"$1" "$1"; then
echo "Error: The branches origin/$1 and $1 have differences."
exit 1
fi
}
un_snapshot_version() {
sed -i "s/\($1\s*=\s*[0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/" gradle.properties
}
current_version() {
awk -F '=' '/version\s*=\s*[0-9.]*/ {gsub(/^ +| +$/,"",$2); print $2}' gradle.properties
}
stage_files() {
for file in "$@"; do
if git diff --exit-code --quiet -- "$file"; then
echo "No changes in $file"
else
git add "$file"
echo "Changes in $file staged for commit"
fi
done
}
commit_change() {
stage_files gradle.properties
git commit -m "[RELEASE] - $1"
git push --porcelain origin master
}
add_release_tag() {
gitTag="v$(current_version)"
git tag -a "$gitTag" -m "Release version $gitTag"
git push --porcelain origin "$gitTag"
}
snapshot_version() {
new_version="$(current_version | awk -F '.' '{print $1 "." $2+1 ".0"}')"
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
}
# check and prepare for release
check_active_branch master
check_uncommitted_changes
prepare_environment
check_last_commit
check_differences master
# un-snapshot version for release
un_snapshot_version version
un_snapshot_version catalog
# release changes and prepare for next release
commit_change "release version: $(current_version)"
add_release_tag
snapshot_version
commit_change 'bump version'

View File

@@ -1,5 +1,7 @@
package ltd.hlaeja.test
import java.util.UUID
import org.assertj.core.api.AbstractAssert
import org.assertj.core.api.AbstractStringAssert
@Suppress("unused")
@@ -13,3 +15,12 @@ fun <SELF : AbstractStringAssert<SELF>?> AbstractStringAssert<SELF>.compareToFil
?: throw UnsupportedOperationException(
"Attempted to compare assertion object to context of a file but expected file was not found: $file",
)
@Suppress("unused")
fun <SELF : AbstractAssert<SELF, ACTUAL>?, ACTUAL> AbstractAssert<SELF, ACTUAL>.isEqualToUuid(
uuid: String,
): SELF = try {
UUID.fromString(uuid).let { expected -> this.isEqualTo(expected) }
} catch (e: IllegalArgumentException) {
throw UnsupportedOperationException("Invalid UUID string provided: $uuid", e)
}

View File

@@ -0,0 +1,18 @@
package ltd.hlaeja.test.container
import ltd.hlaeja.test.container.extension.KafkaPostgresTestExtension
import ltd.hlaeja.test.container.postgres.PostgresTestListener
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.TestExecutionListeners.MergeMode
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(KafkaPostgresTestExtension::class)
@ContextConfiguration(initializers = [KafkaPostgresTestExtension::class])
@TestExecutionListeners(
listeners = [PostgresTestListener::class],
mergeMode = MergeMode.MERGE_WITH_DEFAULTS,
)
annotation class KafkaPostgresTestContainer

View File

@@ -1,11 +1,11 @@
package ltd.hlaeja.test.container
import ltd.hlaeja.test.container.extension.KafkaTestExtension
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.test.context.ContextConfiguration
@Suppress("unused")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@ExtendWith(PostgresExtension::class)
@ContextConfiguration(initializers = [PostgresInitializer::class])
annotation class PostgresContainer
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(KafkaTestExtension::class)
@ContextConfiguration(initializers = [KafkaTestExtension::class])
annotation class KafkaTestContainer

View File

@@ -1,74 +0,0 @@
package ltd.hlaeja.test.container
import java.io.BufferedReader
import java.io.InputStreamReader
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.springframework.core.io.ClassPathResource
import org.springframework.test.context.junit.jupiter.SpringExtension
import kotlinx.coroutines.runBlocking
import io.r2dbc.spi.Connection
import io.r2dbc.spi.ConnectionFactory
import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitFirstOrNull
class PostgresExtension : BeforeAllCallback, AfterAllCallback {
override fun beforeAll(context: ExtensionContext) {
executeSqlFile(context, "postgres/data.sql")
}
override fun afterAll(context: ExtensionContext) {
executeSqlFile(context, "postgres/reset.sql")
}
private fun executeSqlFile(
context: ExtensionContext,
resourceSourcePath: String,
) = runBlocking {
if (ClassPathResource(resourceSourcePath).exists()) {
executeSqlStatements(
postgresConnection(context),
makeSqlStatements(ClassPathResource(resourceSourcePath)),
)
}
}
@Suppress("TooGenericExceptionThrown")
private suspend fun postgresConnection(
context: ExtensionContext,
) = SpringExtension.getApplicationContext(context)
.getBean(ConnectionFactory::class.java)
.create()
.awaitFirstOrElse { throw RuntimeException("Connection factory could not be created") }
private suspend fun executeSqlStatements(
connection: Connection,
statements: List<String>,
) {
try {
statements.forEach { statement ->
connection.createStatement(statement)
.execute()
.awaitFirstOrNull()
}
} finally {
connection.close().awaitFirstOrNull()
}
}
private fun makeSqlStatements(
classPathResource: ClassPathResource,
): List<String> = classPathResource.inputStream.use { inputStream ->
BufferedReader(InputStreamReader(inputStream))
.lines()
.filter { it.isNotEmpty() && !it.startsWith("--") }
.map { it.trim() }
.toList()
.joinToString(" ")
.split(';')
.filter { it.isNotBlank() }
.map { "${it.trim()};" }
}
}

View File

@@ -1,32 +0,0 @@
package ltd.hlaeja.test.container
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.core.io.ClassPathResource
import org.testcontainers.containers.PostgreSQLContainer
@Suppress("unused")
class PostgresInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
postgres().apply {
TestPropertyValues.of(
"spring.r2dbc.url=r2dbc:pool:postgresql://$host:$firstMappedPort/$databaseName",
"spring.r2dbc.username=$username",
"spring.r2dbc.password=$password",
).applyTo(applicationContext)
}
}
private fun postgres(): PostgreSQLContainer<*> = PostgreSQLContainer("postgres:17")
.withReuse(true)
.apply {
"postgres/schema.sql".let {
if (ClassPathResource(it).exists()) {
withInitScript(it)
}
}
start()
}
}

View File

@@ -0,0 +1,15 @@
package ltd.hlaeja.test.container
import ltd.hlaeja.test.container.extension.PostgresTestExtension
import ltd.hlaeja.test.container.postgres.PostgresTestListener
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.TestExecutionListeners.MergeMode
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(PostgresTestExtension::class)
@ContextConfiguration(initializers = [PostgresTestExtension::class])
@TestExecutionListeners(listeners = [PostgresTestListener::class], mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
annotation class PostgresTestContainer

View File

@@ -0,0 +1,27 @@
package ltd.hlaeja.test.container.extension
import ltd.hlaeja.test.container.kafka.TestContainerKafka
import ltd.hlaeja.test.container.postgres.TestContainerPostgres
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
class KafkaPostgresTestExtension : BeforeAllCallback, ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
TestPropertyValues
.of(TestContainerPostgres.props() + TestContainerKafka.props())
.applyTo(applicationContext.environment)
}
override fun beforeAll(context: ExtensionContext) {
if (!TestContainerPostgres.postgres.isRunning) {
TestContainerPostgres.postgres.start()
}
if (!TestContainerKafka.kafka.isRunning) {
TestContainerKafka.kafka.start()
}
}
}

View File

@@ -0,0 +1,21 @@
package ltd.hlaeja.test.container.extension
import ltd.hlaeja.test.container.kafka.TestContainerKafka
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
class KafkaTestExtension : BeforeAllCallback, ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
TestPropertyValues.of(TestContainerKafka.props()).applyTo(applicationContext.environment)
}
override fun beforeAll(context: ExtensionContext) {
if (!TestContainerKafka.kafka.isRunning) {
TestContainerKafka.kafka.start()
}
}
}

View File

@@ -0,0 +1,21 @@
package ltd.hlaeja.test.container.extension
import ltd.hlaeja.test.container.postgres.TestContainerPostgres
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.ConfigurableApplicationContext
class PostgresTestExtension : BeforeAllCallback, ApplicationContextInitializer<ConfigurableApplicationContext> {
override fun initialize(applicationContext: ConfigurableApplicationContext) {
TestPropertyValues.of(TestContainerPostgres.props()).applyTo(applicationContext.environment)
}
override fun beforeAll(context: ExtensionContext) {
if (!TestContainerPostgres.postgres.isRunning) {
TestContainerPostgres.postgres.start()
}
}
}

View File

@@ -0,0 +1,13 @@
package ltd.hlaeja.test.container.kafka
import org.testcontainers.kafka.ConfluentKafkaContainer
object TestContainerKafka {
val kafka: ConfluentKafkaContainer = ConfluentKafkaContainer("confluentinc/cp-kafka:8.0.0")
.withReuse(true)
fun props(): Map<String, String> = mapOf(
"spring.kafka.bootstrap-servers" to kafka.bootstrapServers,
)
}

View File

@@ -0,0 +1,24 @@
package ltd.hlaeja.test.container.postgres
import ltd.hlaeja.test.container.postgres.TestContainerPostgres.sqlFile
import ltd.hlaeja.test.container.util.hasAnnotation
import org.junit.jupiter.api.Nested
import org.springframework.test.context.TestContext
import org.springframework.test.context.TestExecutionListener
class PostgresTestListener : TestExecutionListener {
override fun beforeTestClass(
context: TestContext,
) {
if (context.testClass.hasAnnotation<Nested>()) return
sqlFile("postgres/data.sql", context)
}
override fun afterTestClass(
context: TestContext,
) {
if (context.testClass.hasAnnotation<Nested>()) return
sqlFile("postgres/reset.sql", context)
}
}

View File

@@ -0,0 +1,68 @@
package ltd.hlaeja.test.container.postgres
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import kotlinx.coroutines.runBlocking
import ltd.hlaeja.test.container.util.isResourceFile
import org.springframework.r2dbc.core.DatabaseClient
import org.springframework.r2dbc.core.await
import org.springframework.test.context.TestContext
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.utility.DockerImageName
object TestContainerPostgres {
val postgres = PostgreSQLContainer(DockerImageName.parse("postgres:17"))
.withReuse(true)
.apply {
withDatabaseName("testdb")
withUsername("test")
withPassword("test")
"postgres/schema.sql".isResourceFile()?.let { withInitScript(it.path) }
}
fun props(): Map<String, String> = postgres.let {
mapOf(
"spring.r2dbc.url" to "r2dbc:postgresql://${it.host}:${it.firstMappedPort}/${it.databaseName}",
"spring.r2dbc.username" to it.username,
"spring.r2dbc.password" to it.password,
)
}
fun sqlFile(
sqlFile: String,
context: TestContext,
): Unit = runBlocking {
sqlFile.isResourceFile()
?.inputStream
?.use {
executeSqlStatements(
makeSqlStatements(it),
context.applicationContext.getBean(DatabaseClient::class.java),
)
}
}
@Suppress("TooGenericExceptionThrown", "SqlSourceToSinkFlow")
private suspend fun executeSqlStatements(
statements: List<String>,
databaseClient: DatabaseClient,
) = try {
statements.forEach { databaseClient.sql(it).await() }
} catch (e: Exception) {
throw RuntimeException("Failed to execute SQL statements", e)
}
private fun makeSqlStatements(
inputStream: InputStream,
): List<String> = BufferedReader(InputStreamReader(inputStream))
.lines()
.filter { it.isNotEmpty() && !it.startsWith("--") }
.map { it.trim() }
.toList()
.joinToString(" ")
.split(';')
.filter { it.isNotBlank() }
.map { "${it.trim()};" }
}

View File

@@ -0,0 +1,12 @@
package ltd.hlaeja.test.container.util
import org.springframework.core.io.ClassPathResource
fun String.isResourceFile(): ClassPathResource? = ClassPathResource(this).let { resource ->
when {
resource.exists() && resource.isReadable -> resource
else -> null
}
}
inline fun <reified T : Annotation> Class<*>.hasAnnotation(): Boolean = getAnnotation(T::class.java) != null