implementation("io.ktor:ktor-client-core:2.3.10")
implementation("io.ktor:ktor-client-java:2.3.10")
- implementation("io.ktor:ktor-client-auth:2.3.10")
implementation("io.ktor:ktor-client-content-negotiation:2.3.10")
implementation("io.ktor:ktor-client-logging:2.3.10")
override val Table = DocumentTable<WebDavToken>()
override suspend fun initialize() {
- Table.index(WebDavToken::holder)
+ Table.index(WebDavToken::holder.ascending)
Table.expire(WebDavToken::validUntil)
}
}
override val Table = DocumentTable<Comment>()
override suspend fun initialize() {
- Table.index(Comment::submittedBy, Comment::submittedAt)
- Table.index(Comment::submittedIn, Comment::submittedAt)
+ Table.index(Comment::submittedBy.ascending, Comment::submittedAt.descending)
+ Table.index(Comment::submittedIn.ascending, Comment::submittedAt.descending)
}
suspend fun getCommentsIn(page: List<String>): Flow<Comment> {
override val Table = DocumentTable<CommentReplyLink>()
override suspend fun initialize() {
- Table.index(CommentReplyLink::originalPost)
- Table.index(CommentReplyLink::replyingPost)
- Table.unique(CommentReplyLink::replyingPost, CommentReplyLink::originalPost)
+ Table.index(CommentReplyLink::originalPost.ascending)
+ Table.index(CommentReplyLink::replyingPost.ascending)
+ Table.unique(CommentReplyLink::replyingPost.ascending, CommentReplyLink::originalPost.ascending)
}
suspend fun updateComment(updatedReply: Id<Comment>, repliesTo: Set<Id<Comment>>, now: Instant) {
const val MONGODB_ID_KEY = "_id"
+enum class IndexSort {
+ ASCENDING,
+ DESCENDING;
+}
+
+typealias IndexSortProperty<T> = Pair<KProperty1<T, *>, IndexSort>
+
+val <T> KProperty1<T, *>.ascending: IndexSortProperty<T>
+ get() = this to IndexSort.ASCENDING
+
+val <T> KProperty1<T, *>.descending: IndexSortProperty<T>
+ get() = this to IndexSort.DESCENDING
+
class DocumentTable<T : DataDocument<T>>(private val kClass: KClass<T>) {
private suspend fun collection() = ConnectionHolder.getDatabase().getCollection(kClass.simpleName!!, kClass.java)
- suspend fun index(vararg properties: KProperty1<T, *>) {
- collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()))
+ suspend fun index(vararg properties: IndexSortProperty<T>) {
+ collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+ when (sort) {
+ IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+ IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+ }
+ }))
}
- suspend fun unique(vararg properties: KProperty1<T, *>) {
- collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()), IndexOptions().unique(true))
+ suspend fun unique(vararg properties: IndexSortProperty<T>) {
+ collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+ when (sort) {
+ IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+ IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+ }
+ }), IndexOptions().unique(true))
}
suspend fun expire(property: KProperty1<T, Instant>) {
collection().createIndex(Indexes.ascending(property.serialName), IndexOptions().expireAfter(0L))
}
- suspend fun indexIf(condition: Bson, vararg properties: KProperty1<T, *>) {
- collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()), IndexOptions().partialFilterExpression(condition))
+ suspend fun indexIf(condition: Bson, vararg properties: IndexSortProperty<T>) {
+ collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+ when (sort) {
+ IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+ IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+ }
+ }), IndexOptions().partialFilterExpression(condition))
}
- suspend fun uniqueIf(condition: Bson, vararg properties: KProperty1<T, *>) {
- collection().createIndex(Indexes.ascending(*properties.map { it.serialName }.toTypedArray()), IndexOptions().unique(true).partialFilterExpression(condition))
+ suspend fun uniqueIf(condition: Bson, vararg properties: IndexSortProperty<T>) {
+ collection().createIndex(Indexes.compoundIndex(properties.map { (prop, sort) ->
+ when (sort) {
+ IndexSort.ASCENDING -> Indexes.ascending(prop.serialName)
+ IndexSort.DESCENDING -> Indexes.descending(prop.serialName)
+ }
+ }), IndexOptions().unique(true).partialFilterExpression(condition))
}
suspend fun put(doc: T) {
return collection().deleteMany(where).deletedCount
}
- suspend fun <T : Any> aggregate(pipeline: List<Bson>, resultClass: KClass<T>): Flow<T> {
+ suspend fun <R : Any> aggregate(pipeline: List<Bson>, resultClass: KClass<R>): Flow<R> {
return collection().aggregate(pipeline, resultClass.java)
}
+
+ suspend inline fun <reified R : Any> aggregate(pipeline: List<Bson>): Flow<R> {
+ return aggregate(pipeline, R::class)
+ }
}
-suspend inline fun <T : DataDocument<T>, reified R : Any> DocumentTable<T>.aggregate(pipeline: List<Bson>) = aggregate(pipeline, R::class)
-
suspend inline fun <T : DataDocument<T>> DocumentTable<T>.getOrPut(id: Id<T>, defaultValue: () -> T): T {
val value = get(id)
return if (value == null) {
}
override suspend fun prepare() {
- table.unique(GridFsEntry::path)
+ table.unique(GridFsEntry::path.ascending)
emptyFileId = getOrCreateEmptyFile()
}
override val Table = DocumentTable<NationData>()
override suspend fun initialize() {
- Table.index(NationData::name)
+ Table.index(NationData::name.ascending)
}
fun unknown(id: Id<NationData>): NationData {
override val Table = DocumentTable<PageVisitData>()
override suspend fun initialize() {
- Table.index(PageVisitData::path)
- Table.unique(PageVisitData::path, PageVisitData::visitor)
- Table.index(PageVisitData::lastVisit)
+ Table.index(PageVisitData::path.ascending)
+ Table.unique(PageVisitData::path.ascending, PageVisitData::visitor.ascending)
+ Table.index(PageVisitData::lastVisit.ascending)
}
suspend fun visit(path: String, visitor: String) {
}
suspend fun totalVisits(path: String): PageVisitTotals {
- return Table.aggregate<_, PageVisitTotals>(
+ return Table.aggregate<PageVisitTotals>(
listOf(
Aggregates.match(Filters.eq(PageVisitData::path.serialName, path)),
Aggregates.group(
import info.mechyrdia.Configuration
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
-import info.mechyrdia.data.StoredFileType
import info.mechyrdia.route.Root
import info.mechyrdia.route.href
import io.ktor.server.application.*
import kotlinx.html.a
import kotlinx.html.li
import kotlinx.html.ul
-import java.text.CollationKey
import java.text.Collator
import java.util.*
-import kotlin.Comparator
data class ArticleNode(val name: String, val title: String, val subNodes: List<ArticleNode>?)
decomposition = Collator.FULL_DECOMPOSITION
}
-private val collationSorter = Comparator<Pair<*, CollationKey>> { (_, a), (_, b) ->
- a.compareTo(b)
-}
-
-fun <T> List<T>.sortedLexically(selector: (T) -> String) = map { it to collator.getCollationKey(selector(it)) }
- .sortedWith(collationSorter)
+fun <T> List<T>.sortedLexically(selector: (T) -> String?) = map { it to collator.getCollationKey(selector(it)) }
+ .sortedBy { it.second }
.map { (it, _) -> it }
private fun List<ArticleNode>.sortedAsArticles() = sortedLexically { it.title }.sortedBy { it.subNodes == null }
context(ApplicationCall)
fun List<ArticleNode>.renderInto(list: UL, base: List<String> = emptyList(), format: LoreArticleFormat = LoreArticleFormat.HTML) {
- for (node in this) {
+ for (node in this)
if (node.isViewable)
list.li {
val nodePath = base + node.name
}
}
}
- }
}
suspend fun StoragePath.toFriendlyPageTitle() = ArticleTitleCache.get(this)
val lorePath = elements.drop(1)
if (lorePath.isEmpty()) return TOC_TITLE
- return lorePath.indices.drop(1).map { index ->
- StoragePath(lorePath.take(index)).toFriendlyPageTitle()
+ return lorePath.indices.mapSuspend { index ->
+ StoragePath(lorePath.take(index + 1)).toFriendlyPageTitle()
}.joinToString(separator = " - ")
}
val variables: MutableMap<String, ParserTree>,
val parent: PreProcessorContext? = null,
) {
- constructor(parent: PreProcessorContext? = null, vararg variables: Pair<String, ParserTree>) : this(mutableMapOf(*variables), parent)
-
operator fun get(name: String): ParserTree = variables[name] ?: parent?.get(name) ?: formatErrorToParserTree("Unable to resolve variable $name")
operator fun set(name: String, value: ParserTree) {
operator fun plus(other: Map<String, ParserTree>) = PreProcessorContext(other.toMutableMap(), this)
- fun toMap(): Map<String, ParserTree> = parent?.toMap().orEmpty() + variables
-
companion object {
operator fun invoke(variables: Map<String, ParserTree>, parent: PreProcessorContext? = null) = PreProcessorContext(variables.toMutableMap(), parent)
- const val PAGE_PATH_KEY = "PAGE_PATH"
- const val INSTANT_NOW_KEY = "INSTANT_NOW"
+ private const val PAGE_PATH_KEY = "PAGE_PATH"
+ private const val INSTANT_NOW_KEY = "INSTANT_NOW"
context(ApplicationCall)
fun defaults() = defaults(StoragePath(request.path()))
}
}),
DEFAULT(PreProcessorLexerTag { env, param, subNodes ->
- param.requireParam("var") { varName ->
+ param.requireParam("default") { varName ->
if (varName in env.context)
env.context[varName]
else env.processTree(subNodes)
}
}),
SET_PARAM(PreProcessorLexerTag { env, param, subNodes ->
- param.requireParam("var") { varName ->
+ param.requireParam("set_param") { varName ->
val paramValue = env.context[varName].treeToText()
val withParams = subNodes.map { node ->
if (node is ParserTreeNode.Tag && node.param == null)
}
}),
MATH(PreProcessorMathOperators),
- LOGIC(PreProcessorLogicOperator),
+ LOGIC(PreProcessorLogicVariadicOperator),
+ FORMAT(PreProcessorFormatter),
TEST(PreProcessorInputTest),
JSON_PARSE(PreProcessorLexerTag { _, param, subNodes ->
param.forbidParam("json_parse") {
}),
FILTER(PreProcessorLexerTag { env, param, subNodes ->
param.requireParam("filter") { scriptName ->
- PreProcessorScriptLoader.runScriptSafe(scriptName, env.processTree(subNodes), env) {
+ PreProcessorScriptLoader.runScriptSafe(scriptName, env.processTree(subNodes).unparse(), env) {
it.renderInBBCode()
}
}
return runScriptWithBindings(scriptName, mapOf("args" to groovyArgs), env, errorHandler)
}
- suspend fun runScriptSafe(scriptName: String, input: ParserTree, env: AsyncLexerTagEnvironment<PreProcessorContext, PreProcessorSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
- return runScriptWithBindings(scriptName, mapOf("text" to input.unparse()), env, errorHandler)
+ suspend fun runScriptSafe(scriptName: String, input: String, env: AsyncLexerTagEnvironment<PreProcessorContext, PreProcessorSubject>, errorHandler: (Exception) -> ParserTree): ParserTree {
+ return runScriptWithBindings(scriptName, mapOf("text" to input), env, errorHandler)
}
}
object PreProcessorMathOperators : PreProcessorFunctionLibrary("math") {
override val functions: Map<String, PreProcessorFunction> = mapOf(
"neg" to PreProcessorMathUnaryOperator(Double::unaryMinus),
+ "inv" to PreProcessorMathUnaryOperator { 1.0 / it },
"sin" to PreProcessorMathUnaryOperator(::sin),
"cos" to PreProcessorMathUnaryOperator(::cos),
"tan" to PreProcessorMathUnaryOperator(::tan),
return input.treeToNumberOrNull(String::toDoubleOrNull)
?.let { calculate(it) }
?.numberToTree()
- .formatError("Math operations require numerical inputs, got ${input.unparse()}")
+ .formatError("Math operations require numerical inputs, got in = ${input.unparse()}")
}
fun calculate(input: Double): Double
val right = rightValue.treeToNumberOrNull(String::toDoubleOrNull)
if (left == null || right == null)
- return formatErrorToParserTree("Math operations require numerical inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
+ return formatErrorToParserTree("Math operations require numerical inputs, got left = ${leftValue.unparse()} and right = ${rightValue.unparse()}")
return calculate(left, right).numberToTree()
}
val args = argsList.asPreProcessorList().mapNotNull { it.treeToNumberOrNull(String::toDoubleOrNull) }
if (args.isEmpty() && argsList.isNotEmpty())
- return formatErrorToParserTree("Math operations require numerical inputs, got ${argsList.unparse()}")
+ return formatErrorToParserTree("Math operations require numerical inputs, got in = ${argsList.unparse()}")
return calculate(args).numberToTree()
}
val right = rightValue.treeToNumberOrNull(String::toDoubleOrNull)
if (left == null || right == null)
- return formatErrorToParserTree("Math operations require numerical inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
+ return formatErrorToParserTree("Math operations require numerical inputs, got left = ${leftValue.unparse()} and right = ${rightValue.unparse()}")
return calculate(left, right).booleanToTree()
}
val right = rightValue.treeToBooleanOrNull()
if (left == null || right == null)
- return formatErrorToParserTree("Logical operations require boolean inputs, got ${leftValue.unparse()} and ${rightValue.unparse()}")
+ return formatErrorToParserTree("Logical operations require boolean inputs, got left = ${leftValue.unparse()} and right = ${rightValue.unparse()}")
return calculate(left, right).booleanToTree()
}
fun calculate(left: Boolean, right: Boolean): Boolean
}
-fun interface PreProcessorLogicOperator : PreProcessorFunction {
+fun interface PreProcessorLogicVariadicOperator : PreProcessorFunction {
override suspend fun execute(env: AsyncLexerTagEnvironment<PreProcessorContext, PreProcessorSubject>): ParserTree {
val argsList = env.processTree(env.context["in"])
val args = argsList.asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
if (args.isEmpty() && argsList.isNotEmpty())
- return formatErrorToParserTree("Logical operations require boolean inputs, got ${argsList.unparse()}")
+ return formatErrorToParserTree("Logical operations require boolean inputs, got in = ${argsList.unparse()}")
return calculate(args).booleanToTree()
}
"nand" to PreProcessorLogicBinaryOperator { left, right -> !(left && right) },
"nor" to PreProcessorLogicBinaryOperator { left, right -> !(left || right) },
"xnor" to PreProcessorLogicBinaryOperator { left, right -> !(left xor right) },
- "implies" to PreProcessorLogicBinaryOperator { left, right -> left || !right },
+ "implies" to PreProcessorLogicBinaryOperator { left, right -> !left || right },
- "all" to PreProcessorLogicOperator { inputs -> inputs.all { it } },
- "any" to PreProcessorLogicOperator { inputs -> inputs.any { it } },
- "not_all" to PreProcessorLogicOperator { inputs -> inputs.any { !it } },
- "none" to PreProcessorLogicOperator { inputs -> inputs.none { it } },
+ "all" to PreProcessorLogicVariadicOperator { inputs -> inputs.all { it } },
+ "any" to PreProcessorLogicVariadicOperator { inputs -> inputs.any { it } },
+ "not_all" to PreProcessorLogicVariadicOperator { inputs -> inputs.any { !it } },
+ "none" to PreProcessorLogicVariadicOperator { inputs -> inputs.none { it } },
"count" to PreProcessorFunction { env ->
val argsList = env.processTree(env.context["in"])
val args = argsList.asPreProcessorList().mapNotNull { it.treeToBooleanOrNull() }
if (args.isEmpty() && argsList.isNotEmpty())
- formatErrorToParserTree("Logical operations require boolean inputs, got ${argsList.unparse()}")
+ formatErrorToParserTree("Logical operations require boolean inputs, got in = ${argsList.unparse()}")
else
args.count { it }.numberToTree()
},
"local_instant" to PreProcessorFormatter {
it.toLongOrNull()
?.let { long ->
- listOf(ParserTreeNode.Tag("moment", null, listOf(ParserTreeNode.Text(long.toString()))))
+ listOf(ParserTreeNode.Tag("moment", null, long.toString().textToTree()))
}.formatError("ISO Instant values must be formatted as base-10 long values, got $it")
},
)
override val Table: DocumentTable<RobotUser> = DocumentTable<RobotUser>()
override suspend fun initialize() {
- Table.unique()
+ Table.unique(RobotUser::usedByUser.ascending, RobotUser::usedInMonth.ascending)
}
private fun currentMonth(): Int {