Skip to content

Commit

Permalink
feature: Guild setup and default loading
Browse files Browse the repository at this point in the history
  • Loading branch information
LichtHund committed Oct 6, 2023
1 parent 303d1b4 commit bc8fd8b
Show file tree
Hide file tree
Showing 25 changed files with 413 additions and 115 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
# File-based defaults format
*.iws

# IntelliJ
Expand Down Expand Up @@ -107,7 +107,7 @@ gradle-app.setting
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties

# Cache of project
# Cache of defaults
.gradletasknamecache

# Eclipse Gradle plugin generated files
Expand Down
69 changes: 53 additions & 16 deletions app/src/main/kotlin/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,79 @@
*/
package dev.triumphteam.docsly

import com.zaxxer.hikari.HikariDataSource
import dev.triumphteam.docsly.config.createOrGetConfig
import dev.triumphteam.docsly.controller.apiGuild
import dev.triumphteam.docsly.defaults.Defaults
import dev.triumphteam.docsly.meilisearch.Meili
import dev.triumphteam.docsly.resource.Api
import dev.triumphteam.docsly.project.Projects
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.request.path
import io.ktor.server.resources.Resources
import io.ktor.server.resources.get
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transaction
import kotlinx.serialization.Serializable
import org.slf4j.event.Level

private val config = createOrGetConfig()

public fun main() {
embeddedServer(Netty, port = config.port, host = config.host, module = Application::module).start(wait = true)
embeddedServer(CIO, port = config.port, host = config.host, module = Application::module).start(wait = true)
}

public fun Application.module() {
Database.connect(HikariDataSource(config.postgres.toHikariConfig()))
// Database.connect(HikariDataSource(config.postgres.toHikariConfig()))

/*install(CORS) {
anyHost()
allowHeader(HttpHeaders.ContentType)
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
}*/

install(Meili) { config(config.meili) }
install(Resources)
install(ContentNegotiation) {
json()
}
install(CallLogging) {
level = Level.DEBUG
filter { call -> call.request.path().startsWith("/") }
}

install(Meili) { from(config.meili) }
install(Defaults)
install(Projects)

routing {
get<Api.Index.Search> {

// Setup guild api/routing
apiGuild()

/*get<Api.Index.Search> {
// Here you handle the "api/{index}/search" endpoint
// Getting the index passed
// search<String>(it.parent.index, "")
transaction {
// DocDao.find { DocsTable.name eq "Ass" }.firstOrNull()
}
}
*//*val test = index("test").searchFull<String>("e", null)
// val test = search<List<Test>>(it.parent.index, "e")
println(test)*//*
index("test").addDocuments(
listOf(
Test("Hello"),
Test("there"),
Test("thing"),
),
primaryKey = "boy",
)
call.respond(HttpStatusCode.Accepted)
}*/
}
}

@Serializable
public data class Test(val boy: String)
49 changes: 49 additions & 0 deletions app/src/main/kotlin/controller/ApiGuildController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.triumphteam.docsly.controller

import dev.triumphteam.docsly.api.GuildSetupRequest
import dev.triumphteam.docsly.defaults.Defaults
import dev.triumphteam.docsly.project.Projects
import dev.triumphteam.docsly.resource.GuildApi
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.application.plugin
import io.ktor.server.request.receive
import io.ktor.server.resources.post
import io.ktor.server.response.respond
import io.ktor.server.routing.Routing

public fun Routing.apiGuild() {

val defaults = plugin(Defaults)
val projects = plugin(Projects)

post<GuildApi.Guild.Setup> { setup ->
val guild = setup.parent.guild
val setupDefaults = call.receive<GuildSetupRequest>().defaults

val defaultProjects = defaults.defaultProjects()

// Validate data
setupDefaults.forEach { (project, versions) ->
val defaultVersions = defaultProjects[project] ?: run {
call.respond(HttpStatusCode.BadRequest, "Invalid default project '$project'.")
return@post
}

versions.forEach { version ->
// TODO: Figure better way to get version, and not use folder name
val replaced = version.replace(".", "_")
if (replaced !in defaultVersions) {
call.respond(HttpStatusCode.BadRequest, "Invalid default project version '$version' for project '$project'.")
return@post
}
}
}

// If it goes well nothing will throw and it'll work well!
projects.setupProjects(guild, setupDefaults)

// So we return "accepted"
call.respond(HttpStatusCode.Accepted)
}
}
4 changes: 2 additions & 2 deletions app/src/main/kotlin/database/entity/DocsEntity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.triumphteam.docsly.database.entity

import dev.triumphteam.docsly.database.exposed.serializable
import dev.triumphteam.docsly.serializable.DocElement
import dev.triumphteam.docsly.elements.DocElement
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
Expand All @@ -10,7 +10,7 @@ import org.jetbrains.exposed.sql.Column

public object DocsTable : IdTable<Long>() {
public val guild: Column<Long> = long("guild_id").uniqueIndex()
public val project: Column<String> = text("project").uniqueIndex()
public val project: Column<String> = text("defaults").uniqueIndex()
public val version: Column<String> = text("version").uniqueIndex()
public val location: Column<String> = text("location").uniqueIndex()
public val doc: Column<DocElement> = serializable("doc")
Expand Down
49 changes: 49 additions & 0 deletions app/src/main/kotlin/defaults/Defaults.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.triumphteam.docsly.defaults

import io.ktor.server.application.Application
import io.ktor.server.application.BaseApplicationPlugin
import io.ktor.util.AttributeKey
import java.io.File
import java.io.FileNotFoundException
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.isDirectory
import kotlin.io.path.notExists

public class Defaults {

// Move to separate repo
private val defaultsFolder: Path = Path("data/defaults")
private val defaults: Map<String, Set<String>> = defaultsFolder.toFile().listFiles()?.mapNotNull { project ->
if (!project.isDirectory) return@mapNotNull null

val versions = project.listFiles()?.mapNotNull { version ->
if (!version.isDirectory) null else version.name
}?.toSet() ?: emptySet()

project.name to versions
}?.toMap() ?: emptyMap()

public fun defaultProjects(): Map<String, Set<String>> {
return defaults
}

public fun resolve(project: String, version: String): File {
val folder = defaultsFolder.resolve("$project/$version")
if (folder.notExists() || !folder.isDirectory()) {
throw FileNotFoundException("Could not find file with path '${"$project/$version"}'")
}

return folder.toFile().listFiles()?.find { it.name.endsWith(".json") }
?: throw FileNotFoundException("Could not find json file for '${"$project/$version"}'")
}

public companion object Plugin : BaseApplicationPlugin<Application, Defaults, Defaults> {

override val key: AttributeKey<Defaults> = AttributeKey("defaults")

override fun install(pipeline: Application, configure: Defaults.() -> Unit): Defaults {
return Defaults()
}
}
}
21 changes: 12 additions & 9 deletions app/src/main/kotlin/meilisearch/MeiliClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,35 @@ public class MeiliClient(
public fun index(
uid: String,
primaryKey: String? = null,
searchableAttributes: List<String>? = null,
): Index = Index(uid, primaryKey, searchableAttributes)
): Index = Index(uid, primaryKey)

/** Representation of an index. Contains the needed operations with the index. */
public inner class Index(
public val uid: String,
public val primaryKey: String?,
private val searchableAttributes: List<String>? = null,
) {

/** Create the index. Success even if it already exists. */
public suspend fun create(): HttpResponse = client.post(Indexes()) {
setBody(Create(uid, primaryKey, searchableAttributes))
contentType(ContentType.Application.Json)
setBody(Create(uid, primaryKey))
}.also {
println(it)
}

/** Deletes the current index. Success even if it doesn't exist. */
public suspend fun delete(): HttpResponse = client.delete(Indexes.Uid(uid = uid))

/** Search for specific content in the index. */
public suspend inline fun <reified T> search(query: String, filter: Map<String, String>): List<T> {
public suspend inline fun <reified T> search(query: String, filter: String?): List<T> {
return searchFull<T>(query, filter).hits
}

/** [search] but returns all the data ([SearchResult]) provided by the search. */
public suspend inline fun <reified T> searchFull(query: String, filter: Map<String, String>): SearchResult<T> {
public suspend inline fun <reified T> searchFull(query: String, filter: String?): SearchResult<T> {
// TODO: Handle errors.
return client.post(Indexes.Uid.Search(Indexes.Uid(uid = uid))) {
contentType(ContentType.Application.Json)
setBody(SearchRequest(query, filter))
}.body()
}
Expand All @@ -111,9 +113,11 @@ public class MeiliClient(
}

return client.post(Indexes.Uid.Documents(Indexes.Uid(uid = uid))) {
contentType(ContentType.Application.Json) // Json body
contentType(ContentType.Application.Json)
parameter(PRIMARY_KEY_PARAM, pk)
setBody(documents)
}.also {
println(it)
}
}

Expand Down Expand Up @@ -168,7 +172,6 @@ public class MeiliClient(
public data class Create(
val uid: String,
val primaryKey: String?,
val searchableAttributes: List<String>?,
)

/** Serializable class for the search result from end point. */
Expand All @@ -186,7 +189,7 @@ public class MeiliClient(
@Serializable
public data class SearchRequest(
public val q: String,
public val filter: Map<String, String>,
public val filter: String?,
)

/** Serializable class for the swap end point. */
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/kotlin/meilisearch/MeiliPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package dev.triumphteam.docsly.meilisearch

import dev.triumphteam.docsly.config.MeiliConfig
import io.ktor.http.URLProtocol
import io.ktor.serialization.Configuration
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.BaseApplicationPlugin
Expand All @@ -43,7 +44,7 @@ public class Meili(config: Configuration) {
private var apiKey: String = "masterKey"
private var protocol: URLProtocol = URLProtocol.HTTP

public fun config(config: MeiliConfig) {
public fun from(config: MeiliConfig) {
host = config.host
port = config.port
apiKey = config.apiKey
Expand All @@ -66,7 +67,10 @@ public class Meili(config: Configuration) {
public suspend inline fun <reified T> PipelineContext<*, ApplicationCall>.search(
index: String,
query: String,
filter: Map<String, String> = emptyMap(),
filter: String? = null,
): List<T> = with(this.application.plugin(Meili).client) {
return index(index).search(query, filter)
}

public suspend inline fun PipelineContext<*, ApplicationCall>.index(index: String): MeiliClient.Index =
application.plugin(Meili).client.index(index)
65 changes: 65 additions & 0 deletions app/src/main/kotlin/project/Projects.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dev.triumphteam.docsly.project

import dev.triumphteam.docsly.defaults.Defaults
import dev.triumphteam.docsly.elements.DocElement
import dev.triumphteam.docsly.meilisearch.Meili
import dev.triumphteam.docsly.meilisearch.annotation.PrimaryKey
import io.ktor.server.application.Application
import io.ktor.server.application.BaseApplicationPlugin
import io.ktor.server.application.plugin
import io.ktor.util.AttributeKey
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

public class Projects(private val meili: Meili, private val defaults: Defaults) {

public companion object Plugin : BaseApplicationPlugin<Application, Projects, Projects> {

override val key: AttributeKey<Projects> = AttributeKey("Projects")

override fun install(pipeline: Application, configure: Projects.() -> Unit): Projects {
return Projects(pipeline.plugin(Meili), pipeline.plugin(Defaults))
}

public fun indexKeyFor(guild: String, project: String, version: String): String {
return "$guild:$project:$version"
}
}

private val json = Json {
explicitNulls = false
ignoreUnknownKeys = true
}

public suspend fun setupProjects(guild: String, projects: Map<String, Set<String>>) {
// transaction {
val mapped = projects.mapValues { (project, versions) ->
versions.associateWith { version ->
val jsonFile = defaults.resolve(project, version.replace(".", "_"))
json.decodeFromString<List<DocElement>>(jsonFile.readText())
}
}

runBlocking {
mapped.forEach { (project, versions) ->
versions.forEach { (version, docs) ->
meili.client.index(indexKeyFor(guild, project, version)).addDocuments(
docs.map { doc ->
IndexDocument(doc.location, doc.createReferences())
}
)
}
}
}

// TODO: Postgres
// }
}
}

@Serializable
public data class IndexDocument(
@PrimaryKey public val location: String,
public val references: List<String>,
)
Loading

0 comments on commit bc8fd8b

Please sign in to comment.