Add search terms parser + tests
This commit is contained in:
parent
12619f6550
commit
315a01ea18
@ -2,12 +2,14 @@ package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.app.views.NoteView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import be.simplenotes.domain.usecases.markdown.InvalidMeta
|
||||
import be.simplenotes.domain.usecases.markdown.MissingMeta
|
||||
import be.simplenotes.domain.usecases.markdown.ValidationError
|
||||
import be.simplenotes.search.SearchTerms
|
||||
import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
@ -34,7 +36,9 @@ class NoteController(
|
||||
val html = when (it) {
|
||||
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
|
||||
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
|
||||
is ValidationError -> view.noteEditor(jwtPayload, validationErrors = it.validationErrors, textarea = markdownForm)
|
||||
is ValidationError -> view.noteEditor(jwtPayload,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm)
|
||||
}
|
||||
Response(BAD_REQUEST).html(html)
|
||||
},
|
||||
@ -52,6 +56,13 @@ class NoteController(
|
||||
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag))
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val terms = request.searchTerms()
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val deletedCount = noteService.countDeleted(jwtPayload.userId)
|
||||
return Response(OK).html(view.search(jwtPayload, notes, deletedCount))
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
|
||||
@ -121,4 +132,6 @@ class NoteController(
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Request.searchTerms(): SearchTerms = parseSearchTerms(form("search") ?: "")
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ class Router(
|
||||
"/settings" bind POST to { protected(it, settingsController::settings) },
|
||||
"/export" bind POST to { protected(it, settingsController::export) },
|
||||
"/notes" bind GET to { protected(it, noteController::list) },
|
||||
"/notes" bind POST to { protected(it, noteController::search) },
|
||||
"/notes/new" bind GET to { protected(it, noteController::new) },
|
||||
"/notes/new" bind POST to { protected(it, noteController::new) },
|
||||
"/notes/trash" bind GET to { protected(it, noteController::trash) },
|
||||
|
||||
33
app/src/main/kotlin/utils/SearchTermsParser.kt
Normal file
33
app/src/main/kotlin/utils/SearchTermsParser.kt
Normal file
@ -0,0 +1,33 @@
|
||||
package be.simplenotes.app.utils
|
||||
|
||||
import be.simplenotes.search.SearchTerms
|
||||
|
||||
private val titleRe = """title:['"](?<title>.*?)['"]""".toRegex()
|
||||
private val outerTitleRe = """(?<title>title:['"].*?['"])""".toRegex()
|
||||
|
||||
private val tagRe = """tag:['"](?<tag>.*?)['"]""".toRegex()
|
||||
private val outerTagRe = """(?<tag>tag:['"].*?['"])""".toRegex()
|
||||
|
||||
fun parseSearchTerms(input: String): SearchTerms {
|
||||
val title: String? = titleRe.find(input)?.groups?.get("title")?.value
|
||||
val tag: String? = tagRe.find(input)?.groups?.get("tag")?.value
|
||||
var c: String = input
|
||||
|
||||
if (title != null) {
|
||||
val titleGroup = outerTitleRe.find(input)?.groups?.get("title")?.value
|
||||
titleGroup?.let { c = c.replace(it, "") }
|
||||
}
|
||||
|
||||
if (tag != null) {
|
||||
val tagGroup = outerTagRe.find(input)?.groups?.get("tag")?.value
|
||||
tagGroup?.let { c = c.replace(it, "") }
|
||||
}
|
||||
|
||||
val content = c.trim().ifEmpty { null }
|
||||
|
||||
return SearchTerms(
|
||||
title = title,
|
||||
tag = tag,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@ -55,20 +55,7 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
||||
tag: String?,
|
||||
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
|
||||
div("container mx-auto p-4") {
|
||||
div("flex justify-between mb-4") {
|
||||
h1("text-2xl underline") { +"Notes" }
|
||||
span {
|
||||
a(
|
||||
href = "/notes/trash",
|
||||
classes = "underline font-semibold"
|
||||
) { +"Trash ($numberOfDeletedNotes)" }
|
||||
a(
|
||||
href = "/notes/new",
|
||||
classes = "ml-2 btn btn-green"
|
||||
) { +"New" }
|
||||
}
|
||||
|
||||
}
|
||||
noteListHeader(numberOfDeletedNotes)
|
||||
if (notes.isNotEmpty())
|
||||
noteTable(notes)
|
||||
else
|
||||
@ -81,11 +68,22 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
||||
}
|
||||
}
|
||||
|
||||
fun search(
|
||||
jwtPayload: JwtPayload,
|
||||
notes: List<PersistedNoteMetadata>,
|
||||
numberOfDeletedNotes: Int,
|
||||
) = renderPage("Notes", jwtPayload = jwtPayload) {
|
||||
div("container mx-auto p-4") {
|
||||
noteListHeader(numberOfDeletedNotes)
|
||||
noteTable(notes)
|
||||
}
|
||||
}
|
||||
|
||||
fun trash(
|
||||
jwtPayload: JwtPayload,
|
||||
notes: List<PersistedNoteMetadata>,
|
||||
currentPage: Int,
|
||||
numberOfPages: Int
|
||||
numberOfPages: Int,
|
||||
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
|
||||
div("container mx-auto p-4") {
|
||||
div("flex justify-between mb-4") {
|
||||
|
||||
19
app/src/main/kotlin/views/components/NoteListHeader.kt
Normal file
19
app/src/main/kotlin/views/components/NoteListHeader.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package be.simplenotes.app.views.components
|
||||
|
||||
import kotlinx.html.*
|
||||
|
||||
fun DIV.noteListHeader(numberOfDeletedNotes: Int) {
|
||||
div("flex justify-between mb-4") {
|
||||
h1("text-2xl underline") { +"Notes" }
|
||||
span {
|
||||
a(
|
||||
href = "/notes/trash",
|
||||
classes = "underline font-semibold"
|
||||
) { +"Trash ($numberOfDeletedNotes)" }
|
||||
a(
|
||||
href = "/notes/new",
|
||||
classes = "ml-2 btn btn-green"
|
||||
) { +"New" }
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/src/test/kotlin/utils/SearchTermsParserKtTest.kt
Normal file
39
app/src/test/kotlin/utils/SearchTermsParserKtTest.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package be.simplenotes.app.utils
|
||||
|
||||
import be.simplenotes.search.SearchTerms
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import java.util.stream.Stream
|
||||
|
||||
internal class SearchTermsParserKtTest {
|
||||
|
||||
private fun createResult(
|
||||
input: String,
|
||||
title: String? = null,
|
||||
tag: String? = null,
|
||||
content: String? = null,
|
||||
): Pair<String, SearchTerms> = input to SearchTerms(title, tag, content)
|
||||
|
||||
@Suppress("Unused")
|
||||
private fun results() = Stream.of(
|
||||
createResult("title:'example'", title = "example"),
|
||||
createResult("title:'example with words'", title = "example with words"),
|
||||
createResult("title:'example with words'", title = "example with words"),
|
||||
createResult("""title:"double quotes"""", title = "double quotes"),
|
||||
createResult("title:'example' something else", title = "example", content = "something else"),
|
||||
createResult("tag:'example'", tag = "example"),
|
||||
createResult("tag:'example' title:'other'", title = "other", tag = "example"),
|
||||
createResult("blah blah tag:'example' title:'other'", title = "other", tag = "example", content = "blah blah"),
|
||||
createResult("tag:'example' middle title:'other'", title = "other", tag = "example", content = "middle"),
|
||||
createResult("tag:'example' title:'other' end", title = "other", tag = "example", content = "end"),
|
||||
createResult("tag:'example abc' title:'other with words' this is the end ", title = "other with words", tag = "example abc", content = "this is the end"),
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("results")
|
||||
fun `valid search parser`(case: Pair<String, SearchTerms>) {
|
||||
assertThat(parseSearchTerms(case.first)).isEqualTo(case.second)
|
||||
}
|
||||
|
||||
}
|
||||
@ -11,6 +11,7 @@ import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
|
||||
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||
import be.simplenotes.search.NoteSearcher
|
||||
import be.simplenotes.search.SearchTerms
|
||||
import java.util.*
|
||||
|
||||
class NoteService(
|
||||
@ -83,6 +84,8 @@ class NoteService(
|
||||
searcher.indexNotes(id, notes)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
||||
}
|
||||
|
||||
data class PaginatedNotes(val pages: Int, val notes: List<PersistedNoteMetadata>)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user