add selector

This commit is contained in:
InsanusMokrassar 2022-09-06 23:56:58 +06:00
parent cf5a4c0f61
commit f3f7761bf9
16 changed files with 361 additions and 17 deletions

View File

@ -0,0 +1,22 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.ratings")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@ -0,0 +1,21 @@
package dev.inmo.plaguposter.ratings.selector
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
class DefaultSelector (
private val config: SelectorConfig,
private val repo: RatingsRepo
) : Selector {
override suspend fun take(n: Int): List<PostId> {
val result = mutableListOf<PostId>()
do {
val selected = config.active ?.ratings ?.select(repo, result) ?: break
result.add(selected)
} while (result.size < n)
return result.toList()
}
}

View File

@ -0,0 +1 @@
package dev.inmo.plaguposter.ratings.selector

View File

@ -0,0 +1,7 @@
package dev.inmo.plaguposter.ratings.selector
import dev.inmo.plaguposter.posts.models.PostId
interface Selector {
suspend fun take(n: Int = 1): List<PostId>
}

View File

@ -0,0 +1,104 @@
package dev.inmo.plaguposter.ratings.selector.models
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.random.Random
@Serializable
data class RatingConfig(
val min: Rating?,
val max: Rating?,
val prefer: Prefer
) {
suspend fun select(repo: RatingsRepo, exclude: List<PostId>): PostId? {
var reversed: Boolean = false
var count: Int? = null
when (prefer) {
Prefer.Max -> {
reversed = true
count = 1
}
Prefer.Min -> {
reversed = false
count = 1
}
Prefer.Random -> {
reversed = false
count = null
}
}
val posts = when(min) {
null -> {
when (max) {
null -> {
repo.keys(
count ?.let { Pagination(0, it) } ?: FirstPagePagination(repo.count().toInt()),
reversed
).results.filterNot {
it in exclude
}
}
else -> {
repo.getPostsWithRatingLessEq(max, exclude = exclude).keys
}
}
}
else -> {
when (max) {
null -> {
repo.getPostsWithRatingGreaterEq(min, exclude = exclude).keys
}
else -> {
repo.getPosts(min .. max, reversed, count, exclude = exclude).keys
}
}
}
}
return when (prefer) {
Prefer.Max,
Prefer.Min -> posts.firstOrNull()
Prefer.Random -> posts.randomOrNull()
}
}
@Serializable(Prefer.Serializer::class)
sealed interface Prefer {
val type: String
@Serializable(Serializer::class)
object Max : Prefer { override val type: String = "max" }
@Serializable(Serializer::class)
object Min : Prefer { override val type: String = "min" }
@Serializable(Serializer::class)
object Random : Prefer { override val type: String = "random" }
object Serializer : KSerializer<Prefer> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): Prefer {
val identifier = decoder.decodeString().lowercase()
return values.first { it.type.lowercase() == identifier }
}
override fun serialize(encoder: Encoder, value: Prefer) {
encoder.encodeString(value.type.lowercase())
}
}
companion object {
val values = arrayOf(Max, Min, Random)
}
}
}

View File

@ -0,0 +1,11 @@
package dev.inmo.plaguposter.ratings.selector.models
import kotlinx.serialization.Serializable
@Serializable
data class SelectorConfig(
val items: List<SelectorConfigItem>
) {
val active: SelectorConfigItem?
get() = items.firstOrNull { it.time.isActive }
}

View File

@ -0,0 +1,9 @@
package dev.inmo.plaguposter.ratings.selector.models
import kotlinx.serialization.Serializable
@Serializable
data class SelectorConfigItem(
val time: TimeConfig,
val ratings: RatingConfig
)

View File

@ -0,0 +1,36 @@
package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable
class TimeConfig(
@Serializable(TimeSerializer::class)
val from: Time,
@Serializable(TimeSerializer::class)
val to: Time
) {
@Transient
val range = from .. to
val isActive: Boolean
get() = DateTime.now().time in range
object TimeSerializer : KSerializer<Time> {
val format = TimeFormat("HH:mm")
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): Time {
return format.parseTime(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: Time) {
encoder.encodeString(format.format(value))
}
}
}

View File

@ -0,0 +1,14 @@
package dev.inmo.plaguposter.ratings.selector
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) }
single<Selector> { DefaultSelector(get(), get()) }
}
}

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.plaguposter.ratings.selector"/>

View File

@ -7,4 +7,6 @@ import kotlin.jvm.JvmInline
@JvmInline @JvmInline
value class Rating( value class Rating(
val double: Double val double: Double
) ) : Comparable<Rating> {
override fun compareTo(other: Rating): Int = double.compareTo(other.double)
}

View File

@ -4,4 +4,24 @@ import dev.inmo.micro_utils.repos.ReadKeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
interface ReadRatingsRepo : ReadKeyValueRepo<PostId, Rating> interface ReadRatingsRepo : ReadKeyValueRepo<PostId, Rating> {
suspend fun getPosts(
range: ClosedRange<Rating>,
reversed: Boolean = false,
count: Int? = null,
exclude: List<PostId> = emptyList()
): Map<PostId, Rating>
suspend fun getPostsWithRatingGreaterEq(
then: Rating,
reversed: Boolean = false,
count: Int? = null,
exclude: List<PostId> = emptyList()
): Map<PostId, Rating>
suspend fun getPostsWithRatingLessEq(
then: Rating,
reversed: Boolean = false,
count: Int? = null,
exclude: List<PostId> = emptyList()
): Map<PostId, Rating>
}

View File

@ -1,24 +1,81 @@
package dev.inmo.plaguposter.ratings.exposed package dev.inmo.plaguposter.ratings.exposed
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction
class ExposedRatingsRepo( class ExposedRatingsRepo (
database: Database database: Database
) : RatingsRepo, KeyValueRepo<PostId, Rating> by ExposedKeyValueRepo( ) : RatingsRepo, AbstractExposedKeyValueRepo<PostId, Rating>(
database, database,
{ text("post_id") },
{ double("rating") },
"ratings" "ratings"
).withMapper( ) {
{ string }, override val keyColumn = text("post_id")
{ double }, val ratingsColumn = double("rating")
{ PostId(this) }, override val selectById: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
{ Rating(this) } override val selectByValue: SqlExpressionBuilder.(Rating) -> Op<Boolean> = { ratingsColumn.eq(it.double) }
) override val ResultRow.asKey: PostId
get() = get(keyColumn).let(::PostId)
override val ResultRow.asObject: Rating
get() = get(ratingsColumn).let(::Rating)
override fun update(k: PostId, v: Rating, it: UpdateStatement) {
it[ratingsColumn] = v.double
}
override fun insert(k: PostId, v: Rating, it: InsertStatement<Number>) {
it[keyColumn] = k.string
it[ratingsColumn] = v.double
}
private fun Query.optionallyLimit(limit: Int?) = if (limit == null) {
this
} else {
limit(limit)
}
override suspend fun getPosts(
range: ClosedRange<Rating>,
reversed: Boolean,
count: Int?,
exclude: List<PostId>
): Map<PostId, Rating> = transaction(database) {
select {
ratingsColumn.greaterEq(range.start.double).and(
ratingsColumn.lessEq(range.endInclusive.double)
).and(
keyColumn.notInList(exclude.map { it.string })
)
}.optionallyLimit(count).optionallyReverse(reversed).map {
it.asKey to it.asObject
}
}.toMap()
override suspend fun getPostsWithRatingGreaterEq(
then: Rating,
reversed: Boolean,
count: Int?,
exclude: List<PostId>
) = transaction(database) {
select { ratingsColumn.greaterEq(then.double).and(keyColumn.notInList(exclude.map { it.string })) }.optionallyLimit(count).optionallyReverse(reversed).map {
it.asKey to it.asObject
}
}.toMap()
override suspend fun getPostsWithRatingLessEq(
then: Rating,
reversed: Boolean,
count: Int?,
exclude: List<PostId>
): Map<PostId, Rating> = transaction(database) {
select { ratingsColumn.lessEq(then.double).and(keyColumn.notInList(exclude.map { it.string })) }.optionallyLimit(count).optionallyReverse(reversed).map {
it.asKey to it.asObject
}
}.toMap()
}

View File

@ -15,6 +15,7 @@ dependencies {
api project(":plaguposter.triggers.command") api project(":plaguposter.triggers.command")
api project(":plaguposter.ratings") api project(":plaguposter.ratings")
api project(":plaguposter.ratings.source") api project(":plaguposter.ratings.source")
api project(":plaguposter.ratings.selector")
api libs.psql api libs.psql
} }

View File

@ -24,5 +24,42 @@
}, },
"autoAttach": true, "autoAttach": true,
"ratingOfferText": "What do you think about it?" "ratingOfferText": "What do you think about it?"
},
"selector": {
"items": [
{
"time": {
"from": "00:00",
"to": "00:00"
},
"rating": {
"min": 0.0,
"max": 1.0,
"prefer": "random"
}
},
{
"time": {
"from": "00:00",
"to": "00:00"
},
"rating": {
"min": 0.0,
"max": 1.0,
"prefer": "min"
}
},
{
"time": {
"from": "00:00",
"to": "00:00"
},
"rating": {
"min": 0.0,
"max": 1.0,
"prefer": "max"
}
}
]
} }
} }

View File

@ -6,6 +6,7 @@ String[] includes = [
":posts_registrar", ":posts_registrar",
":ratings", ":ratings",
":ratings:source", ":ratings:source",
":ratings:selector",
":triggers:command", ":triggers:command",
// ":settings", // ":settings",
":runner" ":runner"