Skip to content

Commit b5cee0e

Browse files
authored
Modularize Integrations (#4)
1 parent 94e40c2 commit b5cee0e

File tree

91 files changed

+735
-463
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+735
-463
lines changed

api/build.gradle.kts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.jvm)
3+
alias(libs.plugins.kotlin.serialization)
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
implementation(database.hikaricp)
12+
api(libraries.kotlinx.serialization)
13+
api(libraries.kotlinx.datetime)
14+
implementation(database.exposed.core)
15+
api(libraries.jobrunr)
16+
}

src/main/kotlin/me/snoty/backend/User.kt api/src/main/kotlin/me/snoty/backend/User.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package me.snoty.backend
22

3+
import java.util.*
34
import kotlinx.serialization.Serializable
45
import me.snoty.backend.utils.UUIDSerializer
5-
import java.util.*
66

77
@Serializable
88
data class User(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package me.snoty.backend.scheduling
2+
3+
import org.jobrunr.jobs.lambdas.JobRequest as JobRunrRequest
4+
import org.jobrunr.jobs.lambdas.JobRequestHandler as JobRunrRequestHandler
5+
6+
typealias JobRequest = JobRunrRequest
7+
typealias JobRequestHandler<R> = JobRunrRequestHandler<R>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package me.snoty.backend.scheduling
2+
3+
import org.jobrunr.jobs.lambdas.JobRequest
4+
5+
interface Scheduler {
6+
fun scheduleJob(id: String, job: () -> Unit)
7+
8+
fun scheduleJob(id: String, job: JobRequest)
9+
}

src/main/kotlin/me/snoty/backend/utils/DateUtils.kt api/src/main/kotlin/me/snoty/backend/utils/DateUtils.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package me.snoty.backend.utils
22

3-
import java.time.LocalDateTime
43
import java.time.LocalDate
4+
import java.time.LocalDateTime
55
import java.time.format.DateTimeFormatter
66
import java.time.format.DateTimeParseException as JavaDateTimeParseException
77

88
class DateParseException(message: String) : IllegalArgumentException("Invalid date: $message")
99

1010
object DateUtils {
11-
fun parseUntisFormat(date: String): LocalDate {
11+
fun parseIsoDate(date: String): LocalDate {
1212
return try {
1313
LocalDate.parse(date, DateTimeFormatter.ISO_DATE)
1414
} catch (e: JavaDateTimeParseException) {
@@ -20,7 +20,7 @@ object DateUtils {
2020
class DateTimeParseException(message: String) : IllegalArgumentException("Invalid date time: $message")
2121

2222
object DateTimeUtils {
23-
fun parseUntisFormat(dateTime: String): LocalDateTime {
23+
fun parseIsoDateTime(dateTime: String): LocalDateTime {
2424
return try {
2525
LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_DATE_TIME)
2626
} catch (e: JavaDateTimeParseException) {

build.gradle.kts

+12-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ testing {
5050
useJUnitJupiter()
5151

5252
dependencies {
53-
// testing
53+
// API (contains things like Config)
54+
// for some reason, transitive dependencies aren't included in the test classpath
55+
implementation(projects.api)
5456
implementation(tests.junit.api)
5557
runtimeOnly(tests.junit.engine)
5658
runtimeOnly(tests.junit.launcher)
@@ -73,6 +75,9 @@ val devImplementation: Configuration by configurations.getting {
7375
}
7476

7577
dependencies {
78+
implementation(projects.api)
79+
testImplementation(projects.api)
80+
7681
// configuration
7782
implementation(configuration.hoplite.core)
7883
implementation(configuration.hoplite.yaml)
@@ -130,6 +135,12 @@ dependencies {
130135

131136
// dev
132137
devImplementation(dev.keycloak.adminClient)
138+
139+
implementation(projects.integrations.api)
140+
// depend on all integrations by default
141+
subprojects.filter { it.path.startsWith(":integrations:") }.forEach {
142+
implementation(it)
143+
}
133144
}
134145

135146
application {

integrations/api/build.gradle.kts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
2+
3+
plugins {
4+
alias(libs.plugins.kotlin.jvm)
5+
alias(libs.plugins.kotlin.serialization)
6+
}
7+
8+
repositories {
9+
mavenCentral()
10+
}
11+
12+
dependencies {
13+
api(database.exposed.core)
14+
api(database.exposed.jdbc)
15+
api(database.exposed.json)
16+
api(monitoring.micrometer)
17+
api(ktor.client.core)
18+
api(ktor.client.apache)
19+
api(ktor.client.contentNegotiation)
20+
api(ktor.serialization.kotlinx.json)
21+
api(projects.api)
22+
testImplementation(kotlin("test"))
23+
}
24+
25+
archivesName = "integration-api"
26+
27+
subprojects {
28+
archivesName = "integration-${this.name}"
29+
}
30+
31+
tasks.test {
32+
useJUnitPlatform()
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package me.snoty.integration.common
2+
3+
import io.micrometer.core.instrument.MeterRegistry
4+
import me.snoty.backend.User
5+
import me.snoty.integration.common.diff.EntityDiffMetrics
6+
import me.snoty.integration.common.diff.EntityStateTable
7+
import me.snoty.backend.scheduling.JobRequest
8+
import me.snoty.backend.scheduling.Scheduler
9+
import org.jetbrains.exposed.sql.Database
10+
import org.jetbrains.exposed.sql.SchemaUtils
11+
import org.jetbrains.exposed.sql.transactions.transaction
12+
import java.util.concurrent.ScheduledExecutorService
13+
import java.util.concurrent.TimeUnit
14+
import kotlin.reflect.KClass
15+
16+
abstract class AbstractIntegration<S : IntegrationSettings, R : JobRequest>(
17+
final override val name: String,
18+
final override val settingsType: KClass<S>,
19+
private val stateTable: EntityStateTable<*>,
20+
fetcherFactory: IntegrationFetcherFactory<R>,
21+
database: Database,
22+
meterRegistry: MeterRegistry,
23+
private val metricsPool: ScheduledExecutorService,
24+
scheduler: Scheduler
25+
) : Integration {
26+
constructor(
27+
name: String,
28+
settingsType: KClass<S>,
29+
stateTable: EntityStateTable<*>,
30+
fetcherFactory: IntegrationFetcherFactory<R>,
31+
context: IntegrationContext
32+
) : this(
33+
name,
34+
settingsType,
35+
stateTable,
36+
fetcherFactory,
37+
context.database,
38+
context.meterRegistry,
39+
context.metricsPool,
40+
context.scheduler
41+
)
42+
43+
private val entityDiffMetrics: EntityDiffMetrics = EntityDiffMetrics(meterRegistry, database, name, stateTable)
44+
override val fetcher = fetcherFactory.create(entityDiffMetrics)
45+
private val scheduler = IntegrationScheduler(name, scheduler)
46+
47+
override fun start() {
48+
transaction {
49+
SchemaUtils.createMissingTablesAndColumns(stateTable)
50+
}
51+
metricsPool.scheduleAtFixedRate(entityDiffMetrics.Job(), 0, 30, TimeUnit.SECONDS)
52+
scheduleAll()
53+
}
54+
55+
private fun scheduleAll() {
56+
val allSettings = IntegrationConfigTable.getAllIntegrationConfigs<S>(name)
57+
allSettings.forEach(::schedule)
58+
}
59+
60+
abstract fun createRequest(config: IntegrationConfig<S>): JobRequest
61+
abstract fun getInstanceId(config: IntegrationConfig<S>): Int
62+
63+
override fun schedule(user: User, settings: Any) {
64+
@Suppress("UNCHECKED_CAST")
65+
val integrationConfig = IntegrationConfig(user.id, settings as S)
66+
schedule(integrationConfig)
67+
}
68+
69+
private fun schedule(config: IntegrationConfig<S>) {
70+
scheduler.scheduleJob(listOf(getInstanceId(config), config.user), createRequest(config))
71+
}
72+
}

src/main/kotlin/me/snoty/backend/integration/common/InstanceId.kt integrations/api/src/main/kotlin/me/snoty/integration/common/InstanceId.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.backend.integration.common
1+
package me.snoty.integration.common
22

33
/**
44
* Unique ID representing a 3rd party service instance.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package me.snoty.integration.common
2+
3+
import me.snoty.backend.User
4+
import kotlin.reflect.KClass
5+
6+
7+
interface Integration {
8+
val name: String
9+
val settingsType: KClass<out Any>
10+
val fetcher: IntegrationFetcher<*>
11+
12+
fun start()
13+
fun schedule(user: User, settings: Any)
14+
}
15+
16+
interface IntegrationFactory {
17+
fun create(context: IntegrationContext): Integration
18+
}

src/main/kotlin/me/snoty/backend/integration/common/IntegrationConfigTable.kt integrations/api/src/main/kotlin/me/snoty/integration/common/IntegrationConfigTable.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package me.snoty.backend.integration.common
1+
package me.snoty.integration.common
22

3-
import io.ktor.serialization.kotlinx.json.*
3+
import kotlinx.serialization.json.Json
44
import org.jetbrains.exposed.dao.id.IdTable
55
import org.jetbrains.exposed.sql.SchemaUtils
66
import org.jetbrains.exposed.sql.json.jsonb
@@ -14,7 +14,7 @@ object IntegrationConfigTable : IdTable<Long>() {
1414

1515
val user = uuid("user")
1616
private val integrationType = varchar("integration_type", 255)
17-
private val settings = jsonb<IntegrationSettings>("config", DefaultJson)
17+
private val settings = jsonb<IntegrationSettings>("config", Json)
1818

1919
init {
2020
transaction {
@@ -24,7 +24,7 @@ object IntegrationConfigTable : IdTable<Long>() {
2424

2525
fun <S> getAllIntegrationConfigs(integrationType: String) = transaction {
2626
select(settings, user)
27-
.where { this@IntegrationConfigTable.integrationType eq integrationType }
27+
.where { IntegrationConfigTable.integrationType eq integrationType }
2828
.map { row ->
2929
@Suppress("UNCHECKED_CAST")
3030
IntegrationConfig(row[user], row[settings] as S)
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
package me.snoty.backend.integration.common
1+
package me.snoty.integration.common
22

33
import io.micrometer.core.instrument.MeterRegistry
4+
import me.snoty.backend.scheduling.Scheduler
45
import org.jetbrains.exposed.sql.Database
56
import java.util.concurrent.ScheduledExecutorService
67

78
data class IntegrationContext(
89
val database: Database,
910
val meterRegistry: MeterRegistry,
10-
val metricsPool: ScheduledExecutorService
11+
val metricsPool: ScheduledExecutorService,
12+
val scheduler: Scheduler
1113
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package me.snoty.integration.common
2+
3+
import me.snoty.integration.common.diff.EntityDiffMetrics
4+
import me.snoty.backend.scheduling.JobRequest
5+
import me.snoty.backend.scheduling.JobRequestHandler
6+
7+
interface IntegrationFetcher<S : JobRequest> : JobRequestHandler<S>
8+
9+
fun interface IntegrationFetcherFactory<S : JobRequest> {
10+
fun create(entityDiffMetrics: EntityDiffMetrics): IntegrationFetcher<S>
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package me.snoty.integration.common
2+
3+
import me.snoty.backend.scheduling.JobRequest
4+
import me.snoty.backend.scheduling.Scheduler
5+
6+
/**
7+
* Scheduler that delegates scheduling to the provided [scheduler] and prepends the [integrationName] to the job id.
8+
*/
9+
class IntegrationScheduler(private val integrationName: String, private val scheduler: Scheduler) {
10+
fun scheduleJob(idParts: Collection<Any>, job: JobRequest)
11+
// create a job called "integrationName-instanceId-userId"
12+
= scheduler.scheduleJob("$integrationName-${idParts.joinToString("-")}", job)
13+
}

src/main/kotlin/me/snoty/backend/integration/common/IntegrationSettings.kt integrations/api/src/main/kotlin/me/snoty/integration/common/IntegrationSettings.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.backend.integration.common
1+
package me.snoty.integration.common
22

33
import kotlinx.serialization.Serializable
44

src/main/kotlin/me/snoty/backend/integration/common/diff/EntityDiffMetrics.kt integrations/api/src/main/kotlin/me/snoty/integration/common/diff/EntityDiffMetrics.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.backend.integration.common.diff
1+
package me.snoty.integration.common.diff
22

33
import io.micrometer.core.instrument.Counter
44
import io.micrometer.core.instrument.MeterRegistry

src/main/kotlin/me/snoty/backend/integration/common/diff/EntityStateTable.kt integrations/api/src/main/kotlin/me/snoty/integration/common/diff/EntityStateTable.kt

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
package me.snoty.backend.integration.common.diff
1+
package me.snoty.integration.common.diff
22

3-
import io.ktor.serialization.kotlinx.json.*
43
import kotlinx.serialization.json.Json
5-
import me.snoty.backend.integration.common.InstanceId
4+
import me.snoty.integration.common.InstanceId
65
import me.snoty.backend.utils.When
76
import org.jetbrains.exposed.sql.*
87
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
98
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
109
import org.jetbrains.exposed.sql.json.jsonb
10+
import org.jetbrains.exposed.sql.transactions.transaction
1111

1212
const val ID = "id"
1313
abstract class EntityStateTable<ID>(
14-
json: Json = DefaultJson
14+
json: Json = Json
1515
) : Table() {
1616
abstract val id: Column<ID>
1717
private val instanceId = integer("instance_id")
@@ -53,7 +53,7 @@ abstract class EntityStateTable<ID>(
5353
return entity.diff(result[stateQuery])
5454
}
5555

56-
fun compareAndUpdateState(instanceId: InstanceId, entity: IUpdatableEntity<ID>): DiffResult {
56+
fun compareAndUpdateState(instanceId: InstanceId, entity: IUpdatableEntity<ID>): DiffResult = transaction {
5757
val diffResult = compareState(instanceId, entity)
5858
when (diffResult) {
5959
// entity has changed since last time
@@ -77,8 +77,8 @@ abstract class EntityStateTable<ID>(
7777
is DiffResult.Deleted -> {
7878
deleteWhere(op=deleteIdentifier(instanceId, entity))
7979
}
80-
else -> return diffResult
80+
else -> return@transaction diffResult
8181
}
82-
return diffResult
82+
return@transaction diffResult
8383
}
8484
}

src/main/kotlin/me/snoty/backend/integration/common/diff/Updatable.kt integrations/api/src/main/kotlin/me/snoty/integration/common/diff/Updatable.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.backend.integration.common.diff
1+
package me.snoty.integration.common.diff
22

33
import kotlinx.serialization.json.JsonObject
44

src/main/kotlin/me/snoty/backend/integration/common/diff/UpdatableEntity.kt integrations/api/src/main/kotlin/me/snoty/integration/common/diff/UpdatableEntity.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.backend.integration.common.diff
1+
package me.snoty.integration.common.diff
22

33
/**
44
* Represents an entity that can be updated.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package me.snoty.integration.common.jsonrpc
2+
3+
import io.ktor.http.*
4+
5+
val ContentType.Application.JsonRpc: ContentType
6+
get() = ContentType("application", "json-rpc")

src/main/kotlin/me/snoty/backend/integration/common/jsonrpc/JsonRpcResponse.kt integrations/api/src/main/kotlin/me/snoty/integration/common/jsonrpc/JsonRpcResponse.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.backend.integration.common.jsonrpc
1+
package me.snoty.integration.common.jsonrpc
22

33
import kotlinx.serialization.SerialName
44
import kotlinx.serialization.Serializable

integrations/moodle/build.gradle.kts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.jvm)
3+
alias(libs.plugins.kotlin.serialization)
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
compileOnly(projects.integrations.api)
12+
}

0 commit comments

Comments
 (0)