COCO is designed to facilitate the use of Spring WebFlux, Batch, WebClient, Redis, Flyway, TestContainer and jOOQ.
You can use COCO by @CocoWebApplication
@CocoWebApplication
class Application
@CocoWebApplication imports configuration class for web application (such as JsonConfiguration, WebConfig, FlywayConfiguration ...) and also enables the use of SpringBoot.
@SpringBootApplication
@EnableCocoApplication
@Import(
JsonConfiguration::class,
WebConfig::class,
FlywayConfiguration::class,
WebClientConfiguration::class,
ApiUtilsFactory::class,
EventNotificationBusInjector::class,
DslContextInjector::class,
ReactiveRequestContextInjector::class,
)
annotation class CocoWebApplication()
You can handle Effect (which is data structure of Arrow(FP Library)) by using 'handle'
suspend fun <A> handle(
successCode: HttpStatus = HttpStatus.OK,
handler: Effect<ApiError, A>,
): ResponseEntity<A> {
return handler.fold({ throw ApiError.ApiException(it) }, { result = ResponseEntity.status(successCode).body(it) })
}
Handler example
@PostMapping("/login")
suspend fun login(
@RequestBody request: LoginRequest,
) = handle {
service
.login(
LoginCommand(
username = request.username,
password = request.password,
),
).bindOrRaise {
when (it) {
LoginError.NotFoundUser -> raiseNotFound("사용자가 존재하지 않습니다")
LoginError.PasswordMismatch -> raiseBadRequest("비밀번호가 일치하지 않습니다")
}
}
You can use Flyway simply by only adding YAML configuration.
spring:
flyway:
location: { your sql file location }
You can use TestContainer simply by only adding YAML configuration.
test:
db:
username: { test-user }
password: { test-password }
schema: { test-schema }
engine: mariadb
endpoint: localhost
# classpath 하위에 만들 것
flyway-sql-location: db/migration
flyway-schema-history: { flyway_schema_history }
You can use redis by @Import(RedisConfig::class)
@CocoWebApplication
@Import(RedisConfig::class)
class Application
app:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
You have to do is implements SimpleRedisRepositoryBase
You can customize 'READ' method (e.g retrieveByKey)
class RedisRepositoryImpl(private val redissonReactiveClient: RedissonReactiveClient) :
SimpleRedisRepositoryBase<Entity>() {
override fun generateKey(entity: Entity): String {}
override fun delete(entity: Entity): Effect<Nothing, LocalDateTime> {}
override fun save(entity: Entity): Effect<Nothing, Entity> {}
fun retrieveByKey(key: Key): Effect<NotFound, Entity> {}
}
You can use SpringBatch by @CocoBatchApplication
@CocoBatchApplication
class Application
With COCO, you have to do is only writing batch logic.
COCO provides simple function for batch tasks such as 'intervalNMinutesSchedule(N)'(running batches every N minute) so
that you can use batch easily.
@Configuration
class BatchExample {
@Component
class Job(
private val cocoCoroutineScopeProvider: CocoCoroutineScopeProvider,
private val batchService: BatchService,
) : org.quartz.Job {
override fun execute(jobExecutionContext: JobExecutionContext?) {
cocoCoroutineScopeProvider.provide().launch {
batchService.batch().bindOrNothing()
}
}
}
@Bean
fun batchJobDetail(): JobDetail =
JobBuilder
.newJob(Job::class.java)
.withIdentity(Job::class.qualifiedName)
.storeDurably()
.build()
@Bean
fun batchJobTrigger(): Trigger =
TriggerBuilder
.newTrigger()
.forJob(Job::class.qualifiedName)
.startNow()
.withSchedule(intervalNMinutesSchedule(1))
.build()
}
You can use Spring WebClient by ApiUtils
ApiUtils()
.post(API_ENDPOINT)
.headers(mapOf("Authorization" to authToken))
.body(mapOf("grant_type" to "account_credentials"))
.contentType(MediaType.FormUrlEncode)
.call(Response::class)
You can use jOOQ by implementing SimpleCrudRepositoryBase.
Implementing SimpleCrudRepositoryBase allows you to use CRUD methods.
abstract class SimpleCrudRepositoryBase<Id : EntityId<*>, Entity : EntityBase<Id>, R : Record>(
protected val table: Table<R>,
protected val toJooq: Entity.() -> R,
protected val toDomain: R.() -> Entity,
private val keyColumn: String = "id",
) : JooqRepositoryBase(), Repository<Id, Entity>
---
UserRepository : SimpleCrudRepositoryBase<User.Id, User, UserRecord>
---
UserRepository().save(user)
UserRepository().retrieve(user.id)
UserRepository().delete(user)
You can use 'dynamic query' by implementing SearchRepositoryBase
abstract class SearchRepositoryBase<Id : EntityId<*>, Entity : EntityBase<Id>, R : Record, S : SearchDtoBase>(
table: Table<R>,
toJooq: Entity.() -> R,
toDomain: R.() -> Entity,
val selectConditionBuilder: suspend S.() -> Condition,
) : SearchRepository<Entity, S>, SimpleCrudRepositoryBase<Id, Entity, R>(table, toJooq, toDomain)
---
UserRepository : SearchRepositoryBase<User.Id, User, UserRecord, UserSearchDto>
---
UserRepository().search(
UserSearchDto(
username = "username",
name = "name",
email = "email",
)
)
You can use EventNotificationBus, which enables the transfer of event between different modules.
class A {
fun emitEvent() {
EventNotificationBus().emitNotification(MyCustomEvent())
}
}
---
class B {
fun handleEvent() {
EventNotificationBus()
.notifications()
.handleEvent<MyCustomEvent> { event -> customLogic(event) }
.subscribe()
}
}
You can use pagination by only using 'PageRequest' in your Controller
@GetMapping()
suspend fun page(
@RequestBody request: Request,
page: PageRequest,
) {
...
}