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 2274a92
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package com.diffplug.snapshot
sealed interface SnapshotValue {
val isBinary: Boolean
get() = this is SnapshotValueBinary

fun valueBinary(): ByteArray
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 @@ -32,26 +34,33 @@ internal data class SnapshotValueBinary(val value: ByteArray) : SnapshotValue {
override fun valueString() = throw UnsupportedOperationException("This is a binary value.")
override fun hashCode(): Int = value.contentHashCode()
override fun equals(other: Any?): Boolean =
if (this === other) true
else (other is SnapshotValueBinary && value.contentEquals(other.value))
if (this === other) true
else (other is SnapshotValueBinary && value.contentEquals(other.value))
}

internal data class SnapshotValueString(val value: String) : SnapshotValue {
override fun valueBinary() = throw UnsupportedOperationException("This is a string value.")
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>
val value: SnapshotValue,
private val lensData: ArrayMap<String, SnapshotValue>
) {
/** A sorted immutable map of extra values. */
val lenses: Map<String, SnapshotValue>
get() = lensData

fun lens(key: String, value: ByteArray) =
Snapshot(this.value, lensData.plus(key, SnapshotValue.of(value)))
Snapshot(this.value, lensData.plus(key, SnapshotValue.of(value)))

fun lens(key: String, value: String) =
Snapshot(this.value, lensData.plus(key, SnapshotValue.of(value)))
Snapshot(this.value, lensData.plus(key, SnapshotValue.of(value)))

companion object {
fun of(binary: ByteArray) = Snapshot(SnapshotValue.of(binary), ArrayMap.empty())
Expand All @@ -62,6 +71,7 @@ data class Snapshot(
interface Snapshotter<T> {
fun snapshot(value: T): Snapshot
}

fun parseSS(valueReader: SnapshotValueReader): ArrayMap<String, Snapshot> = TODO()
fun serializeSS(valueWriter: StringWriter, snapshots: ArrayMap<String, Snapshot>): Unit = TODO()

Expand All @@ -73,31 +83,95 @@ 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,86 @@ 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 2274a92

Please sign in to comment.