Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/root' into root
Browse files Browse the repository at this point in the history
  • Loading branch information
weblate committed Aug 20, 2023
2 parents 671c37d + 04a9d8d commit a2e311b
Show file tree
Hide file tree
Showing 18 changed files with 589 additions and 2 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
charset = utf-8
insert_final_newline = true
indent_style = tab
trim_trailing_whitespace = true
max_line_length = 120

[*.yml]
indent_style = space
53 changes: 53 additions & 0 deletions data-adapters/adapter-mongodb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# MongoDB Adapter

This module provides a data adapter (and some utilities) for MongoDB.
This includes some codecs, for commonly-used types - which you can use in your own codec registry if you wish.

# Usage

* **Maven repo:** Maven Central for releases, `https://s01.oss.sonatype.org/content/repositories/snapshots` for
snapshots
* **Maven coordinates:** `com.kotlindiscord.kord.extensions:adapter-mongodb:VERSION`

To switch to the MongoDB data adapter follow these steps:

1. Set the `ADAPTER_MONGODB_URI` environmental variable to a MongoDB connection string.
2. Use the `mongoDB` function to set up the data adapter.

```kotlin
suspend fun main() {
val bot = ExtensibleBot(System.getenv("TOKEN")) {
mongoDB()
}

bot.start()
}
```

3. If you use MongoDB elsewhere in your project, you can use the provided codecs to handle these types:
- `Instant` (Kotlin Datetime)
- `Snowflake` (Kord)
- `StorageType` (KordEx)

```kotlin
// import: com.kotlindiscord.kord.extensions.adapters.mongodb.kordExCodecRegistry
val registry = CodecRegistries.fromRegistries(
kordexCodecRegistry,
MongoClientSettings.getDefaultCodecRegistry(),
)

val client = MongoClient.create(MONGODB_URI)
val database = client.getDatabase("database-name")

val collection = database.getCollection<T>("name")
.withCodecRegistry(registry)
```

For more information on working with codecs,
see [the MongoDB documentation](https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/fundamentals/data-formats/codecs).

# Notes

* All provided codecs store their respective data types as strings in the database.
* If you need to migrate from another data adapter to this one, you should read the code for both data adapters before
writing your own migration code.
31 changes: 31 additions & 0 deletions data-adapters/adapter-mongodb/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
plugins {
`kordex-module`
`published-module`
`dokka-module`
}

metadata {
name = "KordEx Adapters: MongoDB"
description = "KordEx data adapter for MongoDB, including extra codecs"
}

repositories {
maven {
name = "Sonatype Snapshots"
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
}
}

dependencies {
detektPlugins(libs.detekt)
detektPlugins(libs.detekt.libraries)

implementation(libs.kotlin.stdlib)
implementation(libs.kx.coro)
implementation(libs.logging)
implementation(libs.mongodb)

implementation(project(":kord-extensions"))
}

group = "com.kotlindiscord.kord.extensions"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package com.kotlindiscord.kord.extensions.adapters.mongodb

import com.kotlindiscord.kord.extensions.adapters.mongodb.codecs.InstantCodec
import com.kotlindiscord.kord.extensions.adapters.mongodb.codecs.SnowflakeCodec
import com.kotlindiscord.kord.extensions.adapters.mongodb.codecs.StorageTypeCodec
import com.kotlindiscord.kord.extensions.utils.env
import com.mongodb.MongoClientSettings
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistry

internal val MONGODB_URI: String = env("ADAPTER_MONGODB_URI")

public val kordExCodecRegistry: CodecRegistry = CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(
InstantCodec(),
SnowflakeCodec(),
StorageTypeCodec(),
),

MongoClientSettings.getDefaultCodecRegistry(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package com.kotlindiscord.kord.extensions.adapters.mongodb

import com.kotlindiscord.kord.extensions.adapters.mongodb.db.Database
import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder

/**
* Configures the bot to use MongoDB as the data storage adapter.
*
* This method sets up the [MongoDBDataAdapter] as the data adapter for the bot, allowing it to interact with a
* MongoDB database for storing and retrieving data provided by storage units.
*
* Additionally, this method registers a hook with `beforeKoinSetup`, which checks that the database is reachable,
* and runs any pending migrations.
*
* Usage:
*
* ```
* ExtensibleBotBuilder(...) {
* mongoDB()
* }
* ```
*/
public suspend fun ExtensibleBotBuilder.mongoDB() {
dataAdapter(::MongoDBDataAdapter)

hooks {
beforeKoinSetup {
Database.setup()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

@file:Suppress("UNCHECKED_CAST")
@file:OptIn(InternalSerializationApi::class)

package com.kotlindiscord.kord.extensions.adapters.mongodb

import com.kotlindiscord.kord.extensions.adapters.mongodb.db.AdaptedData
import com.kotlindiscord.kord.extensions.adapters.mongodb.db.Database
import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent
import com.kotlindiscord.kord.extensions.storage.Data
import com.kotlindiscord.kord.extensions.storage.DataAdapter
import com.kotlindiscord.kord.extensions.storage.StorageUnit
import com.mongodb.client.model.Filters
import com.mongodb.client.model.Filters.eq
import com.mongodb.client.model.ReplaceOptions
import com.mongodb.kotlin.client.coroutine.MongoCollection
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import org.bson.conversions.Bson

/**
* This class represents a MongoDB data adapter for storing and retrieving data using MongoDB as the underlying
* database for data stored using storage units.
*
* Use the provided [mongoDB] function to add this to your bot, rather than directly referencing the constructor for
* this class.
*/
public class MongoDBDataAdapter : DataAdapter<String>(), KordExKoinComponent {
private val collectionCache: MutableMap<String, MongoCollection<AdaptedData>> = mutableMapOf()

private fun StorageUnit<*>.getIdentifier(): String =
buildString {
append("${storageType.type}/")

if (guild != null) append("guild-$guild/")
if (channel != null) append("channel-$channel/")
if (user != null) append("user-$user/")
if (message != null) append("message-$message/")

append(identifier)
}

private fun getCollection(namespace: String): MongoCollection<AdaptedData> {
val collName = "data-$namespace"

return collectionCache.getOrPut(collName) { Database.getCollection<AdaptedData>(collName) }
}

private fun constructQuery(unit: StorageUnit<*>): Bson =
Filters.and(
listOf(
eq(AdaptedData::identifier.name, unit.identifier),

eq(AdaptedData::type.name, unit.storageType),

eq(AdaptedData::channel.name, unit.channel),
eq(AdaptedData::guild.name, unit.guild),
eq(AdaptedData::message.name, unit.message),
eq(AdaptedData::user.name, unit.user)
)
)

override suspend fun <R : Data> delete(unit: StorageUnit<R>): Boolean {
removeFromCache(unit)

val result = getCollection(unit.namespace)
.deleteOne(constructQuery(unit))

return result.deletedCount > 0
}

override suspend fun <R : Data> get(unit: StorageUnit<R>): R? {
val dataId = unitCache[unit]

if (dataId != null) {
val data = dataCache[dataId]

if (data != null) {
return data as R
}
}

return reload(unit)
}

override suspend fun <R : Data> reload(unit: StorageUnit<R>): R? {
val dataId = unit.getIdentifier()
val result = getCollection(unit.namespace)
.find(constructQuery(unit)).limit(1).firstOrNull()?.data

if (result != null) {
dataCache[dataId] = Json.decodeFromString(unit.dataType.serializer(), result)
unitCache[unit] = dataId
}

return dataCache[dataId] as R?
}

override suspend fun <R : Data> save(unit: StorageUnit<R>): R? {
val data = get(unit) ?: return null

getCollection(unit.namespace).replaceOne(
eq(unit.getIdentifier()),

AdaptedData(
_id = unit.getIdentifier(),

identifier = unit.identifier,

type = unit.storageType,

channel = unit.channel,
guild = unit.guild,
message = unit.message,
user = unit.user,

data = Json.encodeToString(unit.dataType.serializer(), data)
),

ReplaceOptions().upsert(true)
)

return data
}

override suspend fun <R : Data> save(unit: StorageUnit<R>, data: R): R {
val dataId = unit.getIdentifier()

dataCache[dataId] = data
unitCache[unit] = dataId

getCollection(unit.namespace).replaceOne(
eq(unit.getIdentifier()),

AdaptedData(
_id = unit.getIdentifier(),

identifier = unit.identifier,

type = unit.storageType,

channel = unit.channel,
guild = unit.guild,
message = unit.message,
user = unit.user,

data = Json.encodeToString(unit.dataType.serializer(), data)
),

ReplaceOptions().upsert(true)
)

return data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package com.kotlindiscord.kord.extensions.adapters.mongodb.codecs

import kotlinx.datetime.Instant
import org.bson.BsonReader
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext

public class InstantCodec : Codec<Instant> {
override fun decode(reader: BsonReader, decoderContext: DecoderContext): Instant =
Instant.parse(reader.readString())

override fun encode(writer: BsonWriter, value: Instant, encoderContext: EncoderContext) {
writer.writeString(value.toString())
}

override fun getEncoderClass(): Class<Instant> =
Instant::class.java
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package com.kotlindiscord.kord.extensions.adapters.mongodb.codecs

import dev.kord.common.entity.Snowflake
import org.bson.BsonReader
import org.bson.BsonWriter
import org.bson.codecs.Codec
import org.bson.codecs.DecoderContext
import org.bson.codecs.EncoderContext

public class SnowflakeCodec : Codec<Snowflake> {
override fun decode(reader: BsonReader, decoderContext: DecoderContext): Snowflake =
Snowflake(reader.readString())

override fun encode(writer: BsonWriter, value: Snowflake, encoderContext: EncoderContext) {
writer.writeString(value.toString())
}

override fun getEncoderClass(): Class<Snowflake> =
Snowflake::class.java
}
Loading

0 comments on commit a2e311b

Please sign in to comment.