edn.kt
src/jvmMain/kotlin/afield/document/persistence/edn.kt · 640 lines
package afield.document.persistence
import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.runtime.execution.plus
import afield.runtime.lifecycle.end
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.ui.notation.notation
import afield.ui.notation.string
import afield.ui.notation.with
import afield.app.protocol.payload
import afield.audio.synth.*
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.lifecycle.map
import afield.ui.notation.custom
import java.math.BigDecimal
import kotlin.reflect.KClass
An EDN reader and printer, the text format of a document's metadata.edn.
Edn.read and Edn.write convert between EDN text and plain Kotlin values (maps, lists, strings,
numbers), and EdnValue is the parsed tree in between. Tagged values such as #afield/rect are
handled by registering a type with Edn.CustomTypes.
sealed class EdnValue
EDN AST
object Nil : EdnValue()
data class Bool(val value: Boolean) : EdnValue()
data class Number(val token: String) : EdnValue()
data class Str(val value: String) : EdnValue()
data class ListVal(val values: List<EdnValue>) : EdnValue()
data class VectorVal(val values: List<EdnValue>) : EdnValue()
data class SetVal(val values: Set<EdnValue>) : EdnValue()
data class MapVal(val values: Map<EdnValue, EdnValue>) : EdnValue()
data class Symbol(val name: String) : EdnValue()
data class Keyword(val name: String) : EdnValue()
data class Tagged(val tag: String, val value: EdnValue) : EdnValue()
object Edn
Public EDN facade
object Edn {
interface NumberStrategy
Strategy for mapping EDN numeric tokens to JVM numeric types.
Default strategy: - integers -> Int if in range, else Long, else BigDecimal - decimals / scientific notation -> Double
fun fromToken(token: String): Any
object DefaultNumberStrategy : NumberStrategy
override fun fromToken(token: String): Any
override fun fromToken(token: String): Any {
val isFloating =
token.contains('.') || token.contains('e') || token.contains('E')
return if (isFloating) {
token.toDouble()
} else {
try {
val longVal = token.toLong()
if (longVal in Int.MIN_VALUE..Int.MAX_VALUE) {
longVal.toInt()
} else {
longVal
}
} catch (e: NumberFormatException) {
BigDecimal(token)
}
}
}
}
var defaultNumberStrategy: NumberStrategy = DefaultNumberStrategy
Global default strategy used when you don't pass a NumberStrategy explicitly.
class CustomTypes
Registry for custom tagged types.
You can either:
- use register / registerStringPayload to encode as #tag "string"
- use registerEdn to encode as #tag <any-edn-value>
class CustomTypes {
private data class Encoder(
val tag: String,
val encode: (Any) -> EdnValue
)
private val encoders = LinkedHashMap<KClass<*>, Encoder>()
private val decoders = LinkedHashMap<String, (EdnValue) -> Any>()
fun <T : Any> register
Convenience: encode the custom type as #tag "string".
fun <T : Any> register(
type: KClass<T>,
tag: String,
encode: (T) -> String,
decode: (String) -> T
): CustomTypes = registerStringPayload(type, tag, encode, decode)
fun <T : Any> registerStringPayload
fun <T : Any> registerStringPayload(
type: KClass<T>,
tag: String,
encode: (T) -> String,
decode: (String) -> T
): CustomTypes {
encoders[type] = Encoder(tag) { any ->
@Suppress("UNCHECKED_CAST")
EdnValue.Str(encode(any as T))
}
decoders[tag] = { ednValue ->
val str = ednValue as? EdnValue.Str
?: throw IllegalArgumentException(
"Expected EDN string as payload for tag '$tag'"
)
decode(str.value)
}
return this
}
fun <T : Any> registerEdn
Advanced: encode as arbitrary EDN, e.g. #tag [x y z].
fun <T : Any> registerEdn(
type: KClass<T>,
tag: String,
encode: (T) -> EdnValue,
decode: (EdnValue) -> T
): CustomTypes {
encoders[type] = Encoder(tag) { any ->
@Suppress("UNCHECKED_CAST")
encode(any as T)
}
decoders[tag] = { ednValue -> decode(ednValue) }
return this
}
internal fun tryEncode(value: Any): EdnValue.Tagged?
internal fun tryEncode(value: Any): EdnValue.Tagged? {
for ((kClass, encoder) in encoders) {
if (kClass.isInstance(value)) {
val payload = encoder.encode(value)
return EdnValue.Tagged(encoder.tag, payload)
}
}
return null
}
internal fun tryDecode(tag: String, value: EdnValue): Any?
internal fun tryDecode(tag: String, value: EdnValue): Any? {
val decoder = decoders[tag] ?: return null
return decoder(value)
}
}
Public API
fun write(value: Any?, custom: CustomTypes? = null): String
Serialize a single Kotlin value to an EDN string.
fun write(value: Any?, custom: CustomTypes? = null): String {
val ednValue = toEdnValue(value, custom)
return EdnPrinter.print(ednValue)
}
fun writeList(values: List<Any?>, custom: CustomTypes? = null): String
Serialize a list of values as an EDN vector.
fun writeList(values: List<Any?>, custom: CustomTypes? = null): String {
val ednVector = EdnValue.VectorVal(values.map { toEdnValue(it, custom) })
return EdnPrinter.print(ednVector)
}
fun read
Deserialize an EDN string into Kotlin values using the given strategies.
fun read(
edn: String,
custom: CustomTypes? = null,
numberStrategy: NumberStrategy = defaultNumberStrategy
): Any? {
val ednValue = EdnParser(edn).parse()
return fromEdnValue(ednValue, custom, numberStrategy)
}
fun readList
Deserialize a top-level EDN vector or list into List<Any?>.
fun readList(
edn: String,
custom: CustomTypes? = null,
numberStrategy: NumberStrategy = defaultNumberStrategy
): List<Any?> {
val root = EdnParser(edn).parse()
val elements: List<EdnValue> = when (root) {
is EdnValue.VectorVal -> root.values
is EdnValue.ListVal -> root.values
else -> throw IllegalArgumentException(
"Expected EDN vector or list at root, got: $root"
)
}
return elements.map { fromEdnValue(it, custom, numberStrategy) }
}
Kotlin <-> EdnValue conversion
source — 98 lines, from line 214
private fun toEdnValue(value: Any?, custom: CustomTypes?): EdnValue {
if (value == null) return EdnValue.Nil
// Custom tagged types first
if (custom != null) {
val tagged = custom.tryEncode(value)
if (tagged != null) return tagged
}
return when (value) {
is Boolean -> EdnValue.Bool(value)
is Int, is Long, is Short, is Byte ->
EdnValue.Number(value.toString())
is Float, is Double ->
EdnValue.Number(value.toString())
is BigDecimal ->
EdnValue.Number(value.toPlainString())
is String -> EdnValue.Str(value)
is List<*> -> {
val listValues = value.map { toEdnValue(it, custom) }
EdnValue.VectorVal(listValues)
}
is Set<*> -> {
val setValues = value
.map { toEdnValue(it, custom) }
.toCollection(LinkedHashSet())
EdnValue.SetVal(setValues)
}
is Map<*, *> -> {
val mapValues = LinkedHashMap<EdnValue, EdnValue>()
for ((k, v) in value) {
val keyEdn = toEdnValue(k, custom)
val valEdn = toEdnValue(v, custom)
mapValues[keyEdn] = valEdn
}
EdnValue.MapVal(mapValues)
}
else -> throw IllegalArgumentException(
"Don't know how to EDN-encode type ${value::class}"
)
}
}
private fun fromEdnValue(
value: EdnValue,
custom: CustomTypes?,
numberStrategy: NumberStrategy
): Any? {
if (value is EdnValue.Tagged) {
if (custom != null) {
val decoded = custom.tryDecode(value.tag, value.value)
if (decoded != null) return decoded
}
// Unknown tag -> preserve EDN form
return value
}
return when (value) {
EdnValue.Nil -> null
is EdnValue.Bool -> value.value
is EdnValue.Number -> numberStrategy.fromToken(value.token)
is EdnValue.Str -> value.value
is EdnValue.VectorVal ->
value.values.map { fromEdnValue(it, custom, numberStrategy) }
is EdnValue.ListVal ->
value.values.map { fromEdnValue(it, custom, numberStrategy) }
is EdnValue.SetVal ->
value.values.mapTo(LinkedHashSet()) {
fromEdnValue(it, custom, numberStrategy)
}
is EdnValue.MapVal -> {
val result = LinkedHashMap<Any?, Any?>()
for ((k, v) in value.values) {
result[fromEdnValue(k, custom, numberStrategy)] =
fromEdnValue(v, custom, numberStrategy)
}
result
}
is EdnValue.Symbol -> value.name
is EdnValue.Keyword -> ":" + value.name
is EdnValue.Tagged ->
throw IllegalStateException("Tagged value should have been handled earlier")
}
}
}
EDN parser
source — 234 lines, from line 314
private class EdnParser(private val src: String) {
private var index: Int = 0
fun parse(): EdnValue {
skipWhitespaceAndComments()
val value = parseValue()
skipWhitespaceAndComments()
if (index != src.length) {
throw IllegalArgumentException("Unexpected extra input at position $index")
}
return value
}
private fun eof(): Boolean = index >= src.length
private fun currentChar(): Char =
if (!eof()) src[index] else throw IllegalStateException("Unexpected EOF")
private fun peek(offset: Int): Char? {
val i = index + offset
return if (i in src.indices) src[i] else null
}
private fun consume(): Char {
val c = currentChar()
index++
return c
}
private fun expectChar(expected: Char) {
val c: Char? = if (!eof()) src[index] else null
if (c != expected) {
throw IllegalArgumentException("Expected '$expected' at $index, got '$c'")
}
index++
}
private fun skipWhitespaceAndComments() {
while (!eof()) {
val c = src[index]
when {
c == ';' -> {
// Comment until end of line
index++
while (!eof() && src[index] != '\n' && src[index] != '\r') {
index++
}
}
c == ',' || c.isWhitespace() -> index++
else -> return
}
}
}
private fun parseValue(): EdnValue {
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Unexpected EOF while reading EDN")
return when (currentChar()) {
'(' -> parseList()
'[' -> parseVector()
'{' -> parseMap()
'"' -> parseString()
':' -> parseKeyword()
'#' -> {
when (peek(1)) {
'{' -> parseSet()
else -> parseTagged()
}
}
else -> parseTokenValue()
}
}
private fun parseList(): EdnValue {
expectChar('(')
val values = mutableListOf<EdnValue>()
while (true) {
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Unterminated list")
if (currentChar() == ')') {
index++
break
}
values += parseValue()
}
return EdnValue.ListVal(values)
}
private fun parseVector(): EdnValue {
expectChar('[')
val values = mutableListOf<EdnValue>()
while (true) {
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Unterminated vector")
if (currentChar() == ']') {
index++
break
}
values += parseValue()
}
return EdnValue.VectorVal(values)
}
private fun parseMap(): EdnValue {
expectChar('{')
val values = LinkedHashMap<EdnValue, EdnValue>()
while (true) {
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Unterminated map")
if (currentChar() == '}') {
index++
break
}
val key = parseValue()
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Map with odd number of forms")
val value = parseValue()
values[key] = value
}
return EdnValue.MapVal(values)
}
private fun parseSet(): EdnValue {
expectChar('#')
expectChar('{')
val values = LinkedHashSet<EdnValue>()
while (true) {
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Unterminated set")
if (currentChar() == '}') {
index++
break
}
values += parseValue()
}
return EdnValue.SetVal(values)
}
private fun parseTagged(): EdnValue {
expectChar('#')
skipWhitespaceAndComments()
val tagToken = readToken()
skipWhitespaceAndComments()
val value = parseValue()
return EdnValue.Tagged(tagToken, value)
}
private fun parseKeyword(): EdnValue {
expectChar(':')
val start = index
while (!eof() && !isTerminating(src[index]) && !src[index].isWhitespace()) {
index++
}
if (start == index) {
throw IllegalArgumentException("Empty keyword at $index")
}
val name = src.substring(start, index)
return EdnValue.Keyword(name)
}
private fun parseString(): EdnValue {
expectChar('"')
val sb = StringBuilder()
while (!eof()) {
val c = consume()
if (c == '"') {
return EdnValue.Str(sb.toString())
}
if (c == '\\') {
if (eof()) throw IllegalArgumentException("Bad escape at end of input")
val esc = consume()
when (esc) {
'"', '\\', '/' -> sb.append(esc)
'n' -> sb.append('\n')
'r' -> sb.append('\r')
't' -> sb.append('\t')
'b' -> sb.append('\b')
'f' -> sb.append('\u000C')
'u' -> {
if (index + 4 > src.length) {
throw IllegalArgumentException("Invalid unicode escape at $index")
}
val hex = src.substring(index, index + 4)
val codePoint = hex.toInt(16)
sb.append(codePoint.toChar())
index += 4
}
else -> throw IllegalArgumentException("Unknown escape sequence: \\$esc")
}
} else {
sb.append(c)
}
}
throw IllegalArgumentException("Unterminated string literal")
}
private fun parseTokenValue(): EdnValue {
val token = readToken()
return when {
token == "nil" -> EdnValue.Nil
token == "true" -> EdnValue.Bool(true)
token == "false" -> EdnValue.Bool(false)
NUMBER_REGEX.matches(token) -> EdnValue.Number(token)
else -> EdnValue.Symbol(token)
}
}
private fun readToken(): String {
skipWhitespaceAndComments()
if (eof()) throw IllegalArgumentException("Expected token, found EOF")
val start = index
while (!eof() && !isTerminating(src[index]) && !src[index].isWhitespace()) {
index++
}
if (start == index) {
throw IllegalArgumentException("Empty token at $index")
}
return src.substring(start, index)
}
private fun isTerminating(c: Char): Boolean = when (c) {
'(', ')', '[', ']', '{', '}', '"', ';', '#', ',' -> true
else -> false
}
companion object {
private val NUMBER_REGEX =
Regex("^[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+)?$")
}
}
EDN printer
source — 90 lines, from line 550
private object EdnPrinter {
fun print(value: EdnValue): String {
val sb = StringBuilder()
append(value, sb)
return sb.toString()
}
private fun append(value: EdnValue, out: StringBuilder) {
when (value) {
EdnValue.Nil -> out.append("nil")
is EdnValue.Bool -> out.append(if (value.value) "true" else "false")
is EdnValue.Number -> out.append(value.token)
is EdnValue.Str -> appendString(value.value, out)
is EdnValue.ListVal -> {
out.append('(')
appendSequence(value.values, out)
out.append(')')
}
is EdnValue.VectorVal -> {
out.append('[')
appendSequence(value.values, out)
out.append(']')
}
is EdnValue.SetVal -> {
out.append("#{")
appendSequence(value.values.toList(), out)
out.append('}')
}
is EdnValue.MapVal -> {
out.append('{')
appendMap(value.values, out)
out.append('}')
}
is EdnValue.Symbol -> out.append(value.name)
is EdnValue.Keyword -> {
out.append(':')
out.append(value.name)
}
is EdnValue.Tagged -> {
out.append('#')
out.append(value.tag)
out.append(' ')
append(value.value, out)
}
}
}
private fun appendSequence(values: List<EdnValue>, out: StringBuilder) {
values.forEachIndexed { index, v ->
if (index > 0) out.append(' ')
append(v, out)
}
}
private fun appendMap(values: Map<EdnValue, EdnValue>, out: StringBuilder) {
var first = true
for ((k, v) in values) {
if (!first) out.append(' ')
first = false
append(k, out)
out.append(' ')
append(v, out)
}
}
private fun appendString(s: String, out: StringBuilder) {
out.append('"')
for (c in s) {
when (c) {
'"' -> out.append("\\\"")
'\\' -> out.append("\\\\")
'\n' -> out.append("\\n")
'\r' -> out.append("\\r")
'\t' -> out.append("\\t")
'\b' -> out.append("\\b")
'\u000C' -> out.append("\\f")
else -> out.append(c)
}
}
out.append('"')
}
}