Skip to content

Commit

Permalink
SnapshotValueReader: implement peek/next value
Browse files Browse the repository at this point in the history
- Use a `line` as buffer for doing peek
- Uncomment tests
  • Loading branch information
jknack committed Aug 21, 2023
1 parent 78d2a7b commit 48adb00
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sealed interface SnapshotValue {
fun valueString(): String

companion object {
val EMPTY: SnapshotValue = SnapshotValueEmptyString("")
fun of(binary: ByteArray): SnapshotValue = SnapshotValueBinary(binary)
fun of(string: String): SnapshotValue = SnapshotValueString(string)
}
Expand All @@ -41,6 +42,11 @@ internal data class SnapshotValueString(val value: String) : SnapshotValue {
override fun valueString(): String = value
}

internal data class SnapshotValueEmptyString(val value: String) : SnapshotValue {
override fun valueBinary() = throw UnsupportedOperationException("This is an empty string value.")
override fun valueString() = value
}

data class Snapshot(
val value: SnapshotValue,
private val lensData: ArrayMap<String, SnapshotValue>
Expand Down Expand Up @@ -73,31 +79,96 @@ class SnapshotReader(val valueReader: SnapshotValueReader) {

/** Provides the ability to parse a snapshot file incrementally. */
class SnapshotValueReader(val lineReader: LineReader) {
var line: String? = null

/** The key of the next value, does not increment anything about the reader's state. */
fun peekKey(): String? = TODO()
fun peekKey(): String? {
return nextKey()
}

/** Reads the next value. */
fun nextValue(): SnapshotValue = TODO()
fun nextValue(): SnapshotValue {
// validate key
nextKey()
resetLine()
// read value
var nextLine = nextLine()
val buffer = StringBuilder()
while (nextLine != null) {
if (nextLine.isNotBlank() && nextLine[0] == headerFirstChar) {
break
}
resetLine()
buffer.append(nextLine).append('\n')
// read next
nextLine = nextLine()
}
if (buffer.isEmpty()) {
return SnapshotValue.EMPTY
}
return SnapshotValue.of(bodyEsc.escape(buffer.toString().trim()))
}

/** Same as nextValue, but faster. */
fun skipValue(): Unit = TODO()
private fun nextKey(): String? {
val line =
nextLine()
?:
// TODO: do we really want null?
return null

// TODO: confirm exception type
val startIndex = line.indexOf(headerStart)
val endIndex = line.indexOf(headerEnd)
if (startIndex == -1) {
throw IllegalStateException("Expected '$headerStart' at line:${lineReader.getLineNumber()}")
}
if (endIndex == -1) {
throw IllegalStateException("Expected '$headerEnd' at line:${lineReader.getLineNumber()}")
}
// valid key
val key = line.substring(startIndex + headerStart.length, endIndex)
if (key.startsWith(" ")) {
throw IllegalStateException(
"Leading spaces are disallowed: '$key' at line:${lineReader.getLineNumber()}")
}
if (key.endsWith(" ")) {
throw IllegalStateException(
"Trailing spaces are disallowed: '$key' at line:${lineReader.getLineNumber()}")
}
return nameEsc.escape(key)
}
private fun nextLine(): String? {
if (line == null) {
line = lineReader.readLine()
}
return line
}
private fun resetLine() {
line = null
}

companion object {
fun of(content: String) = SnapshotValueReader(LineReader.forString(content))
fun of(content: ByteArray) = SnapshotValueReader(LineReader.forBinary(content))
private val headerFirstChar = ''
private val headerStart = "╔═ "
private val headerEnd = "═╗"
private val headerEnd = " ═╗"

/**
* https://github.com/diffplug/spotless-snapshot/blob/f63192a84390901a3d3543066d095ea23bf81d21/snapshot-lib/src/commonTest/resources/com/diffplug/snapshot/scenarios_and_lenses.ss#L11-L29
*/
private val nameEsc = PerCharacterEscaper.specifiedEscape("\\\\/∕[(])\nn\tt╔┌╗┐═─")

/** https://github.com/diffplug/spotless-snapshot/issues/2 */
private val bodyEsc = PerCharacterEscaper.selfEscape("\uD801\uDF43\uD801\uDF41")
}
}

expect class LineReader {
fun getLineNumber(): Int
fun readLine(): String
fun readLine(): String?

companion object {
fun forString(content: String): LineReader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package com.diffplug.snapshot

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.startWith
import kotlin.test.Test

class SnapshotValueReaderTest {
Expand Down Expand Up @@ -47,43 +50,78 @@ class SnapshotValueReaderTest {
reader.peekKey() shouldBe "01_singleLineString"
reader.peekKey() shouldBe "01_singleLineString"
reader.nextValue().valueString() shouldBe "this is one line"
// etc
reader.peekKey() shouldBe "02_multiLineStringTrimmed"
reader.nextValue().valueString() shouldBe "Line 1\nLine 2"
reader.peekKey() shouldBe "03_multiLineStringTrailingNewline"
reader.nextValue().valueString() shouldBe "Line 1\nLine 2"
reader.peekKey() shouldBe "04_multiLineStringLeadingNewline"
reader.nextValue().valueString() shouldBe "Line 1\nLine 2"
reader.peekKey() shouldBe "05_notSureHowKotlinMultilineWorks"
reader.nextValue().valueString() shouldBe ""
}

@Test
fun invalidNames() {
/* TODO
╔═name ═╗ error: Expected '╔═ '
╔═ name═╗ error: Expected ' ═╗'
╔═ name ═╗ error: Leading spaces are disallowed: ' name'
╔═ name ═╗ error: Trailing spaces are disallowed: 'name '
╔═ name ═╗ comment okay
╔═ name ═╗okay here too
╔═ name ═╗ okay ╔═ ═╗ (it's the first ' ═╗' that counts)
*/
shouldThrow<IllegalStateException> { SnapshotValueReader.of("╔═name ═╗").peekKey() }
.let { it.message should startWith("Expected '╔═ ' at line:1") }
shouldThrow<IllegalStateException> { SnapshotValueReader.of("╔═ name═╗").peekKey() }
.let { it.message should startWith("Expected ' ═╗' at line:1") }
shouldThrow<IllegalStateException> { SnapshotValueReader.of("╔═ name ═╗").peekKey() }
.let { it.message should startWith("Leading spaces are disallowed: ' name' at line:1") }
shouldThrow<IllegalStateException> { SnapshotValueReader.of("╔═ name ═╗").peekKey() }
.let { it.message should startWith("Trailing spaces are disallowed: 'name ' at line:1") }
SnapshotValueReader.of("╔═ name ═╗ comment okay").peekKey()
SnapshotValueReader.of("╔═ name ═╗okay here too").peekKey()
SnapshotValueReader.of("╔═ name ═╗ okay ╔═ ═╗ (it's the first ' ═╗' that counts)")
.peekKey() shouldBe "name"
}

@Test
fun escapeCharactersInName() {
/* TODO
╔═ test with \∕slash\∕ in name ═╗
╔═ test with \(square brackets\) in name ═╗
╔═ test with \\backslash\\ in name ═╗
╔═ test with \nnewline\n in name ═╗
╔═ test with \ttab\t in name ═╗
╔═ test with \┌\─ ascii art \┐\─ in name ═╗
*/
val reader =
SnapshotValueReader.of(
"""
╔═ test with \∕slash\∕ in name ═╗
╔═ test with \(square brackets\) in name ═╗
╔═ test with \\backslash\\ in name ═╗
╔═ test with \nnewline\n in name ═╗
╔═ test with \ttab\t in name ═╗
╔═ test with \┌\─ ascii art \┐\─ in name ═╗
"""
.trimIndent())
reader.peekKey() shouldBe "test with \\\\∕slash\\\\∕ in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with \\\\(square brackets\\\\) in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with \\\\\\\\backslash\\\\\\\\ in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with \\\\nnewline\\\\n in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with \\\\ttab\\\\t in name"
reader.nextValue().valueString() shouldBe ""
reader.peekKey() shouldBe "test with \\\\\\\\─ ascii art \\\\\\\\─ in name"
reader.nextValue().valueString() shouldBe ""
}

@Test
fun escapeCharactersInBody() {
/* TODO
╔═ ascii art okay ═╗
╔══╗
╔═ escaped iff on first line ═╗
𐝁══╗
╔═ body escape characters ═╗
𐝃𐝁𐝃𐝃 linear a is dead
*/
val reader =
SnapshotValueReader.of(
"""
╔═ ascii art okay ═╗
╔══╗
╔═ escaped iff on first line ═╗
𐝁══╗
╔═ body escape characters ═╗
𐝃𐝁𐝃𐝃 linear a is dead
"""
.trimIndent())
reader.peekKey() shouldBe "ascii art okay"
reader.nextValue().valueString() shouldBe "╔══╗"
reader.peekKey() shouldBe "escaped iff on first line"
reader.nextValue().valueString() shouldBe "\uD801\uDF43\uD801\uDF41══╗"
reader.peekKey() shouldBe "body escape characters"
reader.nextValue().valueString() shouldBe
"\uD801\uDF43\uD801\uDF43\uD801\uDF43\uD801\uDF41\uD801\uDF43\uD801\uDF43\uD801\uDF43\uD801\uDF43 linear a is dead"
}
}

0 comments on commit 48adb00

Please sign in to comment.