Skip to content

Commit

Permalink
use properties iso functions wherever possible; also added first part…
Browse files Browse the repository at this point in the history
… of Tag maintenance.
  • Loading branch information
jvorhauer committed Feb 27, 2024
1 parent 905a8aa commit af82cb6
Show file tree
Hide file tree
Showing 21 changed files with 253 additions and 134 deletions.
3 changes: 1 addition & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ dependencies {
implementation("io.sentry:sentry-logback:7.2.0")

testImplementation("com.typesafe.akka:akka-persistence-testkit_2.13:2.9.1")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.9.22")
testImplementation("org.assertj:assertj-core:3.25.2")
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
Expand All @@ -101,7 +100,7 @@ tasks.withType<JavaCompile> {
}

jacoco {
toolVersion = "0.8.9"
toolVersion = "0.8.11"
}

tasks.jacocoTestReport {
Expand Down
6 changes: 3 additions & 3 deletions deploy/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ spec:
value: /var/local/scb_e93d3b60-4128-4094-8824-a37327f973c4_europe-west1.zip
resources:
limits:
cpu: "2"
memory: "2048Mi"
cpu: "4"
memory: "3072Mi"
requests:
cpu: 500m
memory: "512Mi"
memory: "1024Mi"
livenessProbe:
failureThreshold: 5
httpGet:
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/blog/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.sentry.Sentry
import blog.config.Konfig
import blog.model.loginRoute
import blog.model.notesRoute
import blog.model.tagRoute
import blog.model.tasksRoute
import blog.model.usersRoute
import blog.module.authentication
Expand Down Expand Up @@ -51,6 +52,7 @@ object Main {
tasksRoute(processor, reader, scheduler, kfg)
notesRoute(processor, reader, scheduler, kfg)
info(reader)
tagRoute(processor, reader, scheduler, kfg)
}
}.start(wait = true)
Behaviors.same()
Expand Down
36 changes: 21 additions & 15 deletions src/main/kotlin/blog/model/Note.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import java.time.ZonedDateTime
import akka.Done
import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
import akka.actor.typed.javadsl.AskPattern
import akka.actor.typed.javadsl.AskPattern.ask
import akka.pattern.StatusReply
import io.hypersistence.tsid.TSID
import io.ktor.http.*
Expand All @@ -30,42 +30,47 @@ data class CreateNote(
val title: String,
val body: String,
val replyTo: ActorRef<StatusReply<NoteResponse>>,
val id: String = nextId()
val id: String = nextId
) : Command {
fun toEvent() = NoteCreated(id, user, title, body)
val toEvent get() = NoteCreated(id, user, title, body)
}

data class UpdateNote(val user: String, val id: String, val title: String?, val body: String?, val replyTo: ActorRef<StatusReply<NoteResponse>>): Command {
fun toEvent() = NoteUpdated(id, user, title, body)
val toEvent get() = NoteUpdated(id, user, title, body)
}

data class DeleteNote(val id: String, val rt: ActorRef<StatusReply<Done>>): Command {
fun toEvent() = NoteDeleted(id)
val toEvent get() = NoteDeleted(id)
}

data class NoteCreated(val id: String, val user: String, val title: String, val body: String) : Event {
fun toEntity() = Note(id, user, title, title.slug, body)
fun toResponse() = this.toEntity().toResponse()
val toEntity get() = Note(id, user, title, title.slug, body)
val toResponse get() = this.toEntity.toResponse()
}

data class NoteUpdated(val id: String, val user: String, val title: String?, val body: String?): Event
data class NoteUpdated(val id: String, val user: String, val title: String?, val body: String?): Event {
val timestamp: ZonedDateTime get() = TSID.from(id).instant.atZone(CET)
}

data class NoteDeleted(val id: String): Event

data class NoteDelta(val updated: ZonedDateTime, val what: String)

data class Note(
override val id: String,
val user: String,
val title: String,
val slug: String,
val body: String,
val created: ZonedDateTime = TSID.from(id).instant.atZone(CET),
val updated: ZonedDateTime = znow()
val updated: ZonedDateTime = znow,
val events: List<NoteUpdated> = listOf()
): Entity {
constructor(id: String, user: String, title: String, body: String): this(id, user, title, title.slug, body)
fun update(nu: NoteUpdated): Note = this.copy(
title = nu.title ?: this.title, slug = nu.title?.slug ?: this.slug, body = nu.body ?: this.body, updated = znow()
title = nu.title ?: this.title, slug = nu.title?.slug ?: this.slug, body = nu.body ?: this.body, updated = znow, events = this.events + nu
)
fun toResponse() = NoteResponse(id, user, DTF.format(created), DTF.format(updated), title, slug, body)
fun toResponse() = NoteResponse(id, user, DTF.format(created), DTF.format(updated), title, slug, body, events.map { NoteDelta(it.timestamp, "???") })

override fun equals(other: Any?): Boolean = equals(this, other)
override fun hashCode(): Int = id.hashCode()
Expand All @@ -78,7 +83,8 @@ data class NoteResponse(
val updated: String,
val title: String,
val slug: String,
val body: String
val body: String,
val deltas: List<NoteDelta>
)

fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Scheduler, kfg: Konfig) =
Expand All @@ -87,7 +93,7 @@ fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
post {
val cnr = call.receive<CreateNoteRequest>()
val userId = userIdFromJWT(call) ?: return@post call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
AskPattern.ask(processor, { rt -> cnr.toCommand(userId, rt) }, timeout, scheduler).await().let {
ask(processor, { rt -> cnr.toCommand(userId, rt) }, timeout, scheduler).await().let {
if (it.isSuccess) {
call.response.header("Location", "/api/notes/${it.value.id}")
call.respond(HttpStatusCode.Created, it.value)
Expand All @@ -109,7 +115,7 @@ fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
put {
val unr = call.receive<UpdateNoteRequest>()
val userId = userIdFromJWT(call) ?: return@put call.respond(HttpStatusCode.Unauthorized, "Unauthorized")
AskPattern.ask(processor, { rt -> unr.toCommand(userId, rt) }, timeout, scheduler).await().let {
ask(processor, { rt -> unr.toCommand(userId, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, it.value)
else -> call.respond(HttpStatusCode.BadRequest, it.error.localizedMessage)
Expand All @@ -119,7 +125,7 @@ fun Route.notesRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
delete("{id?}") {
val id = call.parameters["id"] ?: return@delete call.respond(HttpStatusCode.NotFound, "note id not specified")
reader.findNote(id) ?: return@delete call.respond(HttpStatusCode.NotFound, "note not found for $id")
AskPattern.ask(processor, { rt -> DeleteNote(id, rt) }, timeout, scheduler).await().let {
ask(processor, { rt -> DeleteNote(id, rt) }, timeout, scheduler).await().let {
when {
it.isSuccess -> call.respond(HttpStatusCode.OK, mapOf("note" to id))
else -> call.respond(HttpStatusCode.InternalServerError, it.error.localizedMessage)
Expand Down
15 changes: 13 additions & 2 deletions src/main/kotlin/blog/model/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import java.io.Serializable

data class State(
private val users: Map<String, User> = HashMap(9),
private val notes: Map<String, Note> = HashMap(9),
private val tasks: Map<String, Task> = HashMap(9),
private val notes: Map<String, Note> = HashMap(99),
private val tasks: Map<String, Task> = HashMap(99),
private val tags : Map<String, Tag> = HashMap(9),
private val recovered: Boolean = false
): Serializable {
Expand All @@ -28,5 +28,16 @@ data class State(
fun taskCount() : Int = tasks.size
fun deleteTask(id: String) : State = this.copy(tasks = this.tasks.minus(id))

fun save(t: Tag) : State = this.copy(tags = this.tags.minus(t.id).plus(t.id to t))
fun findTag(id: String) : Tag? = tags[id]

inline fun <reified T> find(id: String): T? = when (T::class) {
User::class -> findUser(id) as T?
Note::class -> findNote(id) as T?
Task::class -> findTask(id) as T?
Tag::class -> findTag(id) as T?
else -> null
}

fun setRecovered(): State = this.copy(recovered = true)
}
54 changes: 47 additions & 7 deletions src/main/kotlin/blog/model/Tag.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
package blog.model

import akka.actor.typed.ActorRef
import akka.actor.typed.Scheduler
import akka.actor.typed.javadsl.AskPattern.ask
import akka.pattern.StatusReply
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.future.await
import blog.config.Konfig
import blog.read.Reader

data class Tag(override val id: String, val label: String): Entity {
data class Tag(override val id: String, val label: String) : Entity {
fun toResponse() = TagResponse(id, label)
override fun equals(other: Any?): Boolean = equals(this, other)
override fun hashCode(): Int = id.hashCode()
}

data class CreateTagRequest(val label: String) {
fun toCommand(replyTo: ActorRef<StatusReply<TagResponse>>) = CreateTag(nextId(), label, replyTo)
fun toCommand(replyTo: ActorRef<StatusReply<TagResponse>>) = CreateTag(nextId, label, replyTo)
}

data class TagResponse(override val id: String, val label: String): Response
data class TagResponse(override val id: String, val label: String) : Response

data class CreateTag(val id: String, val label: String, val replyTo: ActorRef<StatusReply<TagResponse>>): Command {
fun toEvent() = TagCreated(id, label)
data class CreateTag(val id: String, val label: String, val replyTo: ActorRef<StatusReply<TagResponse>>) : Command {
val toEvent get() = TagCreated(id, label)
}

data class TagCreated(val id: String, val label: String): Event {
fun toEntity() = Tag(id, label)
data class TagCreated(val id: String, val label: String) : Event {
val toEntity get() = Tag(id, label)
}

fun Route.tagRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Scheduler, kfg: Konfig) {
authenticate(kfg.jwt.realm) {
route("/api/tags") {
post {
val ctr = call.receive<CreateTagRequest>()
ask(processor, { rt -> ctr.toCommand(rt) }, timeout, scheduler).await().let {
if (it.isSuccess) {
call.response.header("Location", "/api/tags/${it.value.id}")
call.respond(HttpStatusCode.Created, it.value)
} else {
call.respond(HttpStatusCode.BadRequest, it.error.localizedMessage)
}
}
}
get {
call.respond(reader.allTags.map { it.toResponse() })
}
get("{id?}") {
val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.NotFound, "no tag id specified")
(reader.find<Tag>(id) ?: return@get call.respond(HttpStatusCode.NotFound, "tag not found for $id")).let {
call.respond(it.toResponse())
}
}
}
}
}
23 changes: 12 additions & 11 deletions src/main/kotlin/blog/model/Task.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,18 @@ data class Task(
val status: TaskStatus = TaskStatus.TODO,
val private: Boolean = true,
val created: ZonedDateTime = TSID.from(id).instant.atZone(CET),
val updated: ZonedDateTime = znow()
val updated: ZonedDateTime = znow
) : Entity {
fun update(tu: TaskUpdated): Task = this.copy(
title = tu.title ?: this.title,
slug = tu.title?.slug ?: this.slug,
body = tu.body ?: this.body,
due = tu.due ?: this.due,
status = tu.status ?: this.status,
updated = znow()
updated = znow
)
fun toResponse() = TaskResponse(id, created.fmt, updated.fmt, user, title, body, due.fmt, status.name)

val toResponse get() = TaskResponse(id, created.fmt, updated.fmt, user, title, body, due.fmt, status.name)
override fun equals(other: Any?): Boolean = equals(this, other)
override fun hashCode(): Int = id.hashCode()
}
Expand All @@ -62,9 +63,9 @@ data class CreateTask(
val body: String,
val due: LocalDateTime,
val replyTo: ActorRef<StatusReply<TaskResponse>>,
val id: String = nextId()
val id: String = nextId
) : Command {
fun toEvent() = TaskCreated(id, user, title, body, due)
val toEvent get() = TaskCreated(id, user, title, body, due)
}

data class UpdateTask(
Expand All @@ -76,11 +77,11 @@ data class UpdateTask(
val status: TaskStatus?,
val replyTo: ActorRef<StatusReply<TaskResponse>>
) : Command {
fun toEvent() = TaskUpdated(user, id, title, body, due, status)
val toEvent get() = TaskUpdated(user, id, title, body, due, status)
}

data class DeleteTask(val id: String, val replyTo: ActorRef<StatusReply<Done>>): Command {
fun toEvent() = TaskDeleted(id)
val toEvent get() = TaskDeleted(id)
}


Expand All @@ -91,8 +92,8 @@ data class TaskCreated(
val body: String,
val due: LocalDateTime
) : Event {
fun toEntity() = Task(id, user, title, title.slug, body, due)
fun toResponse() = toEntity().toResponse()
val toEntity get() = Task(id, user, title, title.slug, body, due)
val toResponse get() = toEntity.toResponse
}

data class TaskUpdated(
Expand Down Expand Up @@ -137,12 +138,12 @@ fun Route.tasksRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
get {
val rows = call.request.queryParameters["rows"]?.toInt() ?: 10
val start = call.request.queryParameters["start"]?.toInt() ?: 0
call.respond(reader.allTasks(rows, start).map { it.toResponse() })
call.respond(reader.allTasks(rows, start).map { it.toResponse })
}
get("{id?}") {
val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.NotFound, "no task id specified")
(reader.find<Task>(id) ?: return@get call.respond(HttpStatusCode.NotFound, "task not found for $id")).let {
call.respond(it.toResponse())
call.respond(it.toResponse)
}
}
put {
Expand Down
14 changes: 7 additions & 7 deletions src/main/kotlin/blog/model/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ data class CreateUser(
val name: String,
val password: String,
val replyTo: ActorRef<StatusReply<User>>,
val id: String = nextId(),
val id: String = nextId,
) : Command {
constructor(rur: RegisterUserRequest, replyTo: ActorRef<StatusReply<User>>) : this(rur.email, rur.name, rur.password, replyTo)

fun toEvent() = UserCreated(id, email, name.encode, password.hashed)
val toEvent get() = UserCreated(id, email, name.encode, password.hashed)
}

data class UpdateUser(
Expand All @@ -59,7 +59,7 @@ data class UpdateUser(
val password: String?,
val replyTo: ActorRef<StatusReply<User>>,
): Command {
fun toEvent() = UserUpdated(id, name, password?.hashed)
val toEvent get() = UserUpdated(id, name, password?.hashed)
}

// Events
Expand All @@ -70,7 +70,7 @@ data class UserCreated(
val name: String,
val password: String,
) : Event {
fun toEntity(): User = User(id, email, name, password)
val toEntity: User get() = User(id, email, name, password)
}

data class UserUpdated(
Expand All @@ -90,7 +90,7 @@ data class User(
val password: String,
val gravatar: String = email.gravatar,
val joined: ZonedDateTime = TSID.from(id).instant.atZone(CET),
val updated: ZonedDateTime = znow()
val updated: ZonedDateTime = znow
) : Entity {
fun toResponse(reader: Reader): UserResponse = UserResponse(
id,
Expand All @@ -102,7 +102,7 @@ data class User(
reader.findTasksForUser(id).map(Task::toResponse)
)
fun update(uu: UserUpdated): User = this.copy(
name = uu.name ?: this.name, password = uu.password ?: this.password, updated = znow()
name = uu.name ?: this.name, password = uu.password ?: this.password, updated = znow
)
override fun hashCode(): Int = id.hashCode()
override fun equals(other: Any?): Boolean = equals(this, other)
Expand Down Expand Up @@ -148,7 +148,7 @@ fun Route.usersRoute(processor: ActorRef<Command>, reader: Reader, scheduler: Sc
authenticate(kfg.jwt.realm) {
get("/tasks") {
val userId = userIdFromJWT(call) ?: return@get call.respondText("Unauthorized", status = Unauthorized)
call.respond(reader.findTasksForUser(userId).map { it.toResponse() })
call.respond(reader.findTasksForUser(userId).map { it.toResponse })
}
get("/me") {
val id = userIdFromJWT(call) ?: return@get call.respondText("Unauthorized", status = Unauthorized)
Expand Down
Loading

0 comments on commit af82cb6

Please sign in to comment.