diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/controller/LineStatusHistoryController.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/controller/LineStatusHistoryController.kt index bd615c49..53302d7e 100644 --- a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/controller/LineStatusHistoryController.kt +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/controller/LineStatusHistoryController.kt @@ -7,22 +7,19 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Produces import io.micronaut.http.annotation.QueryValue import io.micronaut.views.View -import net.twisterrob.blt.data.StaticData import net.twisterrob.blt.io.feeds.trackernet.LineStatusFeed import net.twisterrob.travel.domain.london.status.Feed import net.twisterrob.travel.domain.london.status.api.ParsedStatusItem import net.twisterrob.travel.domain.london.status.api.StatusHistoryRepository -import net.twisterrob.travel.statushistory.viewmodel.LineColor -import net.twisterrob.travel.statushistory.viewmodel.LineStatusModel +import net.twisterrob.travel.statushistory.viewmodel.LineStatusModelMapper import net.twisterrob.travel.statushistory.viewmodel.Result -import net.twisterrob.travel.statushistory.viewmodel.ResultChangeModelMapper import net.twisterrob.travel.statushistory.viewmodel.ResultChangesCalculator import java.util.Date @Controller class LineStatusHistoryController( private val useCase: StatusHistoryRepository, - private val staticData: StaticData, + private val modelMapper: LineStatusModelMapper, ) { @Get("/LineStatusHistory") @@ -40,12 +37,7 @@ class LineStatusHistoryController( .map(ParsedStatusItem::toResult) val changes = ResultChangesCalculator().getChanges(results) - return HttpResponse.ok( - LineStatusModel( - changes.map(ResultChangeModelMapper()::map), - LineColor.AllColors(staticData.lineColors) - ) - ) + return HttpResponse.ok(modelMapper.map(changes)) } } diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/di/Dependencies.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/di/Dependencies.kt index 9db574eb..0ff961f5 100644 --- a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/di/Dependencies.kt +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/di/Dependencies.kt @@ -11,6 +11,7 @@ import net.twisterrob.blt.data.StaticData import net.twisterrob.blt.io.feeds.LocalhostUrlBuilder import net.twisterrob.blt.io.feeds.TFLUrlBuilder import net.twisterrob.blt.io.feeds.URLBuilder +import net.twisterrob.blt.model.LineColors import net.twisterrob.travel.domain.london.status.DomainStatusHistoryRepository import net.twisterrob.travel.domain.london.status.DomainRefreshUseCase import net.twisterrob.travel.domain.london.status.api.FeedParser @@ -53,6 +54,10 @@ class Dependencies { fun staticData(): StaticData = SharedStaticData() + @Singleton + fun lineColors(staticData: StaticData): LineColors = + staticData.lineColors + @Singleton @Requires(notEnv = ["development"]) fun urlBuilder(): URLBuilder = diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpers.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpers.kt index f157be07..b1284c91 100644 --- a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpers.kt +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpers.kt @@ -50,20 +50,7 @@ object HandlebarsHelpers { fun formatDate(value: Date, pattern: String): String = SimpleDateFormat(pattern, Locale.getDefault()).format(value) - /** - * Workaround for `myEnumMap.[Foo]` and `lookup myEnumMap Foo` not working. - * Usage: Replace `(lookup someEnumMap someEnumKey)` with `(lookupEnumMap someEnumMap someEnumKey)`. - * See https://github.com/jknack/handlebars.java/issues/1083. - */ @JvmStatic - fun > lookupEnumMap(map: EnumMap, key: E): Any? = - map[key] - - /** - * Workaround for `myMap.[Foo]` and `lookup myMap Foo` not working, because Handlebars toStrings the keys. - * Usage: Replace `(lookup someEnumMap someEnumKey)` with `(lookupMap someMap someKey)`. - */ - @JvmStatic - fun lookupMap(map: Map, key: K): Any? = - map[key] + fun formatCssColor(value: Int): String = + "#%06X".format(Locale.ROOT, value and @Suppress("detekt.MagicNumber") 0xFFFFFF) } diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColor.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColor.kt deleted file mode 100644 index 6f756d4c..00000000 --- a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColor.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.twisterrob.travel.statushistory.viewmodel - -import net.twisterrob.blt.model.Line -import net.twisterrob.blt.model.LineColors -import java.util.Locale - -class LineColor( - private val colors: LineColors, - val line: Line, -) { - - val foregroundColor: String - get() = line.getForeground(colors).toColorString() - - val backgroundColor: String - get() = line.getBackground(colors).toColorString() - - class AllColors( - private val colors: LineColors, - ) : Iterable { - - override fun iterator(): Iterator = - @OptIn(ExperimentalStdlibApi::class) - object : Iterator { - private val lines = Line.entries - private var current = 0 - - override fun hasNext(): Boolean = - current < lines.size - - override fun next(): LineColor { - if (!hasNext()) throw NoSuchElementException() - return LineColor(colors, lines[current++]) - } - } - } - - companion object { - - private fun Int.toColorString(): String = - "#%06X".format(Locale.ROOT, this and @Suppress("detekt.MagicNumber") 0xFFFFFF) - } -} diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModel.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModel.kt new file mode 100644 index 00000000..340dccf0 --- /dev/null +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModel.kt @@ -0,0 +1,9 @@ +package net.twisterrob.travel.statushistory.viewmodel + +import net.twisterrob.blt.model.Line + +class LineColorsModel( + val line: Line, + val foregroundColor: Int, + val backgroundColor: Int, +) diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModelMapper.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModelMapper.kt new file mode 100644 index 00000000..ddcb41f1 --- /dev/null +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModelMapper.kt @@ -0,0 +1,20 @@ +package net.twisterrob.travel.statushistory.viewmodel + +import jakarta.inject.Inject +import net.twisterrob.blt.model.Line +import net.twisterrob.blt.model.LineColors + +class LineColorsModelMapper @Inject constructor( + private val colors: LineColors, +) { + + @OptIn(ExperimentalStdlibApi::class) + fun map(): List = + Line.entries.map { line -> + LineColorsModel( + line = line, + foregroundColor = line.getForeground(colors), + backgroundColor = line.getBackground(colors), + ) + } +} diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineStatusModelMapper.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineStatusModelMapper.kt new file mode 100644 index 00000000..a3e412ff --- /dev/null +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/LineStatusModelMapper.kt @@ -0,0 +1,15 @@ +package net.twisterrob.travel.statushistory.viewmodel + +import jakarta.inject.Inject + +class LineStatusModelMapper @Inject constructor( + private val resultChangeModelMapper: ResultChangeModelMapper, + private val lineColorsModelMapper: LineColorsModelMapper, +) { + + fun map(changes: List): LineStatusModel = + LineStatusModel( + changes.map(resultChangeModelMapper::map), + lineColorsModelMapper.map() + ) +} diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModel.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModel.kt index f82e7c55..0bf9e516 100644 --- a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModel.kt +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModel.kt @@ -8,7 +8,7 @@ import java.util.Date class LineStatusModel( val feedChanges: List, - val colors: Iterable, + val colors: List, ) class ResultChangeModel( diff --git a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModelMapper.kt b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModelMapper.kt index c68c59f0..bb002d5e 100644 --- a/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModelMapper.kt +++ b/web/status-history/src/main/java/net/twisterrob/travel/statushistory/viewmodel/ResultChangeModelMapper.kt @@ -1,11 +1,13 @@ package net.twisterrob.travel.statushistory.viewmodel +import jakarta.inject.Inject import net.twisterrob.blt.diff.HtmlDiff import net.twisterrob.blt.io.feeds.trackernet.model.LineStatus import net.twisterrob.blt.model.Line import net.twisterrob.travel.statushistory.viewmodel.ResultChangeModel.LineStatusModel -class ResultChangeModelMapper { +class ResultChangeModelMapper @Inject constructor( +) { fun map(changes: Changes): ResultChangeModel = ResultChangeModel( diff --git a/web/status-history/src/main/resources/views/LineStatus.hbs b/web/status-history/src/main/resources/views/LineStatus.hbs index 27677988..dfc678b5 100644 --- a/web/status-history/src/main/resources/views/LineStatus.hbs +++ b/web/status-history/src/main/resources/views/LineStatus.hbs @@ -63,8 +63,8 @@ {{#each colors as | lineColor | }} .line-{{lineColor.line}} { - color: {{lineColor.foregroundColor}}; - background-color: {{lineColor.backgroundColor}}; + color: {{formatCssColor lineColor.foregroundColor}}; + background-color: {{formatCssColor lineColor.backgroundColor}}; } {{/each}} diff --git a/web/status-history/src/test/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpersUnitTest.kt b/web/status-history/src/test/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpersUnitTest.kt new file mode 100644 index 00000000..0e1703d9 --- /dev/null +++ b/web/status-history/src/test/java/net/twisterrob/travel/statushistory/view/handlebars/HandlebarsHelpersUnitTest.kt @@ -0,0 +1,225 @@ +package net.twisterrob.travel.statushistory.view.handlebars + +import com.github.jknack.handlebars.Context +import com.github.jknack.handlebars.Handlebars +import com.github.jknack.handlebars.Options +import com.github.jknack.handlebars.TagType +import com.github.jknack.handlebars.Template +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import java.util.Calendar + +/** + * @see HandlebarsHelpers + */ +@Suppress("ClassName") +class HandlebarsHelpersUnitTest { + + @Nested + inner class assign { + + @Test fun `assign value`() { + val options = options() + + HandlebarsHelpers.assign("myName", "myValue", options) + + val value = Handlebars().compileInline("{{myName}}").apply(options.context) + assertEquals("myValue", value) + } + + @Test fun `assign null`() { + val options = options() + + HandlebarsHelpers.assign("myName", null, options) + + val value = Handlebars().compileInline("{{myName}}").apply(options.context) + assertEquals("", value) + } + + @Test fun `clear after assign`() { + val options = options() + + HandlebarsHelpers.assign("myName", "myValue", options) + HandlebarsHelpers.assign("myName", null, options) + + val value = Handlebars().compileInline("{{myName}}").apply(options.context) + assertEquals("", value) + } + + private fun options(): Options = Options + .Builder( + Handlebars(), + "test", + TagType.SUB_EXPRESSION, + Context.newContext(null), + Template.EMPTY + ) + .build() + } + + @Nested + inner class logical { + + @ParameterizedTest + @CsvSource( + "true, true, true", + "true, false, true", + "false, true, true", + "false, false, false", + ) + fun `or truthTable`(left: Boolean, right: Boolean, expected: Boolean) { + val result = HandlebarsHelpers.or(left, right) + + assertThat(result, equalTo(expected)) + } + + @ParameterizedTest + @CsvSource( + "true, true, true", + "true, false, false", + "false, true, false", + "false, false, false", + ) + fun `and truthTable`(left: Boolean, right: Boolean, expected: Boolean) { + val result = HandlebarsHelpers.and(left, right) + + assertThat(result, equalTo(expected)) + } + + @ParameterizedTest + @CsvSource( + "true, false", + "false, true", + ) + fun `not truthTable`(right: Boolean, expected: Boolean) { + val result = HandlebarsHelpers.not(right) + + assertThat(result, equalTo(expected)) + } + } + + @Nested + inner class empty { + + @ParameterizedTest + @CsvSource( + ", true", + "'', true", + "null, false", + "' ', false", + "asdf, false", + nullValues = [""], + ) + fun test(value: String?, expected: Boolean) { + val result = HandlebarsHelpers.empty(value) + + assertThat(result, equalTo(expected)) + } + } + + @Nested + inner class concat { + + @ParameterizedTest + @CsvSource( + "'', '', ''", + "left, '', left", + "'', right, right", + "left, right, leftright", + ) + fun test(left: String, right: String, expected: String) { + val result = HandlebarsHelpers.concat(left, right) + + assertThat(result, equalTo(expected)) + } + } + + @Nested + inner class add { + + @ParameterizedTest + @CsvSource( + "0, 0, 0", + "0, 1, 1", + "1, 0, 1", + "1, 1, 2", + "12, 34, 46", + "0, -1, -1", + "-5, -6, -11", + "3, -5, -2", + ) + fun test(base: Int, increment: Int, expected: Int) { + val result = HandlebarsHelpers.add(base, increment) + + assertThat(result, equalTo(expected)) + } + } + + @Nested + inner class formatDate { + + @Test fun `format date`() { + val date = Calendar.getInstance().apply { set(2021, Calendar.AUGUST, 3) }.time + + val result = HandlebarsHelpers.formatDate(date, "yyyy-MM-dd") + + assertEquals("2021-08-03", result) + } + + @Test fun `invalid format fails`() { + val date = Calendar.getInstance().apply { set(2021, 8, 3) }.time + + assertThrows { + HandlebarsHelpers.formatDate(date, "invalid") + } + } + } + + @Nested + inner class formatCssColor { + + @Test fun testShortColor1() { + testColors(0x000000AB, "#0000AB") + } + + @Test fun testShortColor2() { + testColors(0x000000CD, "#0000CD") + } + + @Test fun testLongShortColor1() { + testColors(0xFF0000AB.toInt(), "#0000AB") + } + + @Test fun testLongShortColor2() { + testColors(0xFF0000CD.toInt(), "#0000CD") + } + + @Test fun testLongColor1() { + testColors(0xFFAB00CD.toInt(), "#AB00CD") + } + + @Test fun testLongColor2() { + testColors(0xFFCD00EF.toInt(), "#CD00EF") + } + + @Test fun testEdgeZero() { + testColors(0x00000000, "#000000") + } + + @Test fun testEdgeFull() { + testColors(0xFFFFFFFF.toInt(), "#FFFFFF") + } + + private fun testColors(colorInput: Int, expected: String) { + val result = HandlebarsHelpers.formatCssColor(colorInput) + + assertThat(result, equalTo(expected)) + } + } +} diff --git a/web/status-history/src/test/java/net/twisterrob/travel/statushistory/viewmodel/LineColorUnitTest.kt b/web/status-history/src/test/java/net/twisterrob/travel/statushistory/viewmodel/LineColorUnitTest.kt deleted file mode 100644 index 0bacd728..00000000 --- a/web/status-history/src/test/java/net/twisterrob/travel/statushistory/viewmodel/LineColorUnitTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package net.twisterrob.travel.statushistory.viewmodel - -import net.twisterrob.blt.model.Line -import net.twisterrob.blt.model.LineColors -import net.twisterrob.travel.statushistory.viewmodel.LineColor.AllColors -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.anyOf -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.not -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoInteractions -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.`when` -import org.mockito.junit.jupiter.MockitoExtension - -@ExtendWith(MockitoExtension::class) -class LineColorUnitTest { - - @Mock lateinit var colors: LineColors - - @Test fun testShortColor() { - testColors(0x000000AB, "#0000AB", 0x000000CD, "#0000CD") - } - - @Test fun testLongShortColor() { - testColors(0xFF0000AB.toInt(), "#0000AB", 0xFF0000CD.toInt(), "#0000CD") - } - - @Test fun testLongColor() { - testColors(0xFFAB00CD.toInt(), "#AB00CD", 0xFFCD00EF.toInt(), "#CD00EF") - } - - private fun testColors(bgColor: Int, bgExpect: String, fgColor: Int, fgExpect: String) { - `when`(colors.jubileeBackground).thenReturn(bgColor) - `when`(colors.jubileeForeground).thenReturn(fgColor) - - val color = LineColor(colors, Line.Jubilee) - - assertEquals(Line.Jubilee, color.line) - assertEquals(bgExpect, color.backgroundColor) - assertEquals(fgExpect, color.foregroundColor) - - verify(colors, atLeastOnce()).jubileeBackground - verify(colors, atLeastOnce()).jubileeForeground - verifyNoMoreInteractions(colors) - } - - @Test fun testAllColorsGiveAllLines() { - val all = AllColors(colors) - assertThat(all.map { it.line }, equalTo(@OptIn(ExperimentalStdlibApi::class) Line.entries.sorted())) - verifyNoInteractions(colors) - } - - @Test fun testAllColorsHasValidIterator() { - val all = AllColors(colors) - - val iterator = all.iterator() - while (iterator.hasNext()) { - iterator.next() - } - - assertThrows { iterator.next() } - } - - @Test fun testAllColorsConsistentProperties() { - val colors = AllColors(colors) - val it1 = colors.iterator() - val getIt1 = colors.iterator() - val it2 = colors.iterator() - val getIt2 = colors.iterator() - assertThat(it1, not(equalTo(it2))) - assertThat(getIt1, not(equalTo(getIt2))) - assertThat(it1, not(anyOf(equalTo(getIt1), equalTo(getIt2)))) - assertThat(it2, not(anyOf(equalTo(getIt1), equalTo(getIt2)))) - } -} diff --git a/web/status-history/src/test/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModelMapperUnitTest.kt b/web/status-history/src/test/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModelMapperUnitTest.kt new file mode 100644 index 00000000..2b93a6f4 --- /dev/null +++ b/web/status-history/src/test/java/net/twisterrob/travel/statushistory/viewmodel/LineColorsModelMapperUnitTest.kt @@ -0,0 +1,52 @@ +package net.twisterrob.travel.statushistory.viewmodel + +import net.twisterrob.blt.model.Line +import net.twisterrob.blt.model.LineColors +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.junit.jupiter.MockitoExtension + +@ExtendWith(MockitoExtension::class) +class LineColorsModelMapperUnitTest { + + @Mock lateinit var colors: LineColors + + private lateinit var subject: LineColorsModelMapper + + @BeforeEach fun setUp() { + subject = LineColorsModelMapper(colors) + } + + @Test fun testAllColorsGiveAllLines() { + val allLinesInOrder = @OptIn(ExperimentalStdlibApi::class) Line.entries.sorted() + + val result = subject.map() + + val linesCovered = result.map { it.line } + assertThat(linesCovered, equalTo(allLinesInOrder)) + } + + @Test fun testJubileeMapping() { + val fgColor = 0x12345678.toInt() + val bgColor = 0x87654321.toInt() + `when`(colors.jubileeBackground).thenReturn(bgColor) + `when`(colors.jubileeForeground).thenReturn(fgColor) + + val result = subject.map() + + val jubilee = result.single { it.line == Line.Jubilee } + assertEquals(Line.Jubilee, jubilee.line) + assertEquals(bgColor, jubilee.backgroundColor) + assertEquals(fgColor, jubilee.foregroundColor) + + verify(colors).jubileeBackground + verify(colors).jubileeForeground + } +}