-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
740 additions
and
205 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package cc.ekblad.toml | ||
|
||
import cc.ekblad.toml.configuration.TomlSerializerConfigurator | ||
import cc.ekblad.toml.model.TomlDocument | ||
import cc.ekblad.toml.serialization.TomlSerializerConfig | ||
import cc.ekblad.toml.serialization.TomlSerializerState | ||
import cc.ekblad.toml.serialization.writePath | ||
import cc.ekblad.toml.util.JacocoIgnore | ||
import java.io.OutputStream | ||
import java.io.PrintStream | ||
import java.nio.file.Path | ||
import kotlin.io.path.outputStream | ||
|
||
class TomlSerializer internal constructor(private val config: TomlSerializerConfig) { | ||
/** | ||
* Serializes the given [TomlDocument] into a valid TOML document using the receiver [TomlSerializer] | ||
* and writes it to the given [Appendable]. | ||
*/ | ||
fun write(tomlDocument: TomlDocument, output: Appendable) { | ||
TomlSerializerState(config, output).writePath(tomlDocument, emptyList()) | ||
} | ||
|
||
/** | ||
* Serializes the given [TomlDocument] into a valid TOML document using the receiver [TomlSerializer] | ||
* and writes it to the given [Appendable]. | ||
*/ | ||
fun write(tomlDocument: TomlDocument, outputStream: OutputStream) { | ||
write(tomlDocument, PrintStream(outputStream) as Appendable) | ||
} | ||
|
||
/** | ||
* Serializes the given [TomlDocument] into a valid TOML document using the receiver [TomlSerializer] | ||
* and writes it to the given [Appendable]. | ||
*/ | ||
@JacocoIgnore("JaCoCo thinks use isn't being called, even though it also thinks use's argument IS called") | ||
fun write(tomlDocument: TomlDocument, path: Path) { | ||
path.outputStream().use { write(tomlDocument, it) } | ||
} | ||
} | ||
|
||
fun tomlSerializer(configuration: TomlSerializerConfigurator.() -> Unit): TomlSerializer { | ||
val configurator = TomlSerializerConfigurator() | ||
configurator.configuration() | ||
return TomlSerializer(configurator.buildConfig()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
src/main/kotlin/cc/ekblad/toml/configuration/TomlSerializerConfigurator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package cc.ekblad.toml.configuration | ||
|
||
import cc.ekblad.toml.serialization.CollectionSyntax | ||
import cc.ekblad.toml.serialization.InlineListMode | ||
import cc.ekblad.toml.serialization.TomlSerializerConfig | ||
|
||
class TomlSerializerConfigurator internal constructor() { | ||
private var indentStep: Int = TomlSerializerConfig.default.indentStep | ||
private var inlineListMode: ListMode = TomlSerializerConfig.default.inlineListMode | ||
private var preferredTableSyntax: CollectionSyntax = TomlSerializerConfig.default.preferredTableSyntax | ||
private var preferredListSyntax: CollectionSyntax = TomlSerializerConfig.default.preferredListSyntax | ||
|
||
/** | ||
* Indent each nesting level of a multi-line lists by this many spaces. | ||
* | ||
* Default: 4 | ||
*/ | ||
fun indentStep(spaces: Int) { | ||
indentStep = spaces | ||
} | ||
|
||
/** | ||
* How should inline lists be serialized? | ||
* | ||
* - If [ListMode.SingleLine], inline lists are always serialized on a single line. | ||
* - If [ListMode.MultiLine], inline lists are always serialized with one element per line, as long as the TOML | ||
* spec allows it. | ||
* - If [ListMode.Adaptive], inline lists are serialized with one element per line only if they contain one or more | ||
* list or table, as long as the TOML spec allows it. | ||
* | ||
* Default: ListMode.Adaptive | ||
*/ | ||
fun inlineListMode(listMode: ListMode) { | ||
inlineListMode = listMode | ||
} | ||
|
||
/** | ||
* Should tables be generated using table syntax or inline table syntax (i.e. curly brackets)? | ||
* | ||
* Default: [CollectionSyntax.Table] | ||
*/ | ||
fun preferTableSyntax(syntax: CollectionSyntax) { | ||
preferredTableSyntax = syntax | ||
} | ||
|
||
/** | ||
* Should tables be generated using table syntax or inline table syntax (i.e. square brackets)? | ||
* If [CollectionSyntax.Table], lists will still be serialized using inline syntax when required to produce | ||
* correct TOML. | ||
* | ||
* Default: [CollectionSyntax.Table] | ||
*/ | ||
fun preferListSyntax(syntax: CollectionSyntax) { | ||
preferredListSyntax = syntax | ||
} | ||
|
||
internal fun buildConfig(): TomlSerializerConfig = TomlSerializerConfig( | ||
indentStep = indentStep, | ||
inlineListMode = inlineListMode, | ||
preferredTableSyntax = preferredTableSyntax, | ||
preferredListSyntax = preferredListSyntax | ||
) | ||
} | ||
|
||
/** | ||
* When should list elements be separated by line breaks instead of spaces? | ||
*/ | ||
typealias ListMode = InlineListMode | ||
|
||
/** | ||
* Preferred syntax for lists and tables: inline or table? | ||
*/ | ||
typealias CollectionSyntax = CollectionSyntax |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
src/main/kotlin/cc/ekblad/toml/serialization/TomlSerializationAdapter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package cc.ekblad.toml.serialization | ||
|
||
import cc.ekblad.toml.model.TomlDocument | ||
import cc.ekblad.toml.model.TomlException | ||
import cc.ekblad.toml.model.TomlValue | ||
import java.io.OutputStream | ||
import java.io.PrintStream | ||
import java.nio.file.Path | ||
import java.time.format.DateTimeFormatter | ||
import kotlin.io.path.outputStream | ||
|
||
/** | ||
* Serializes the receiver [TomlDocument] into a valid TOML document using the default serializer | ||
* and writes it to the given [Appendable]. | ||
*/ | ||
fun TomlDocument.write(output: Appendable) { | ||
TomlSerializerState(TomlSerializerConfig.default, output).writePath(this, emptyList()) | ||
} | ||
|
||
/** | ||
* Serializes the receiver [TomlDocument] into a valid TOML document using the default serializer | ||
* and writes it to the given [OutputStream]. | ||
*/ | ||
fun TomlDocument.write(outputStream: OutputStream) { | ||
write(PrintStream(outputStream) as Appendable) | ||
} | ||
|
||
/** | ||
* Serializes the receiver [TomlDocument] into a valid TOML document using the default serializer | ||
* and writes it to the file represented by the given [Path]. | ||
*/ | ||
fun TomlDocument.write(path: Path) { | ||
path.outputStream().use { write(it) } | ||
} | ||
|
||
internal fun TomlSerializerState.writePath(value: TomlValue.Map, path: List<String>) { | ||
if (path.isEmpty()) { | ||
writeTopLevel(value) | ||
} else { | ||
value.properties.forEach { | ||
writePath(it.value, path + it.key) | ||
} | ||
} | ||
} | ||
|
||
private fun TomlSerializerState.writeTopLevel(map: TomlValue.Map) { | ||
val (inline, table) = map.properties.entries.partition { (_, element) -> shouldBeInline(element) } | ||
|
||
inline.forEach { (key, value) -> | ||
writeKeyValue(value, listOf(".", key)) | ||
} | ||
|
||
if (inline.isNotEmpty() && table.isNotEmpty()) { | ||
appendLine() | ||
} | ||
|
||
table.fold(true) { first, it -> | ||
when (val value = it.value) { | ||
is TomlValue.List -> { | ||
value.elements.fold(first) { firstInner, element -> | ||
if (!firstInner) { | ||
appendLine() | ||
} | ||
appendLine("[[${encodeKey(it.key)}]]") | ||
writePath(element, listOf(it.key)) | ||
false | ||
} | ||
} | ||
else -> { | ||
if (!first) { | ||
appendLine() | ||
} | ||
appendLine("[${encodeKey(it.key)}]") | ||
writePath(value, listOf(it.key)) | ||
} | ||
} | ||
false | ||
} | ||
} | ||
|
||
private fun TomlSerializerState.writePath(value: TomlValue, path: List<String>) { | ||
when (value) { | ||
is TomlValue.Map -> writePath(value, path) | ||
is TomlValue.List -> writeKeyValue(value, path) | ||
is TomlValue.Primitive -> writeKeyValue(value, path) | ||
} | ||
} | ||
|
||
private fun TomlSerializerState.writeKeyValue(value: TomlValue, path: List<String>) { | ||
append(encodePath(path.drop(1)), " = ") | ||
writeValue(value) | ||
appendLine() | ||
} | ||
|
||
private fun TomlSerializerState.writeValue(value: TomlValue) { | ||
when (value) { | ||
is TomlValue.List -> writeValue(value) | ||
is TomlValue.Map -> writeValue(value) | ||
is TomlValue.Bool -> append(value.value.toString()) | ||
is TomlValue.Double -> append(value.value.toString()) | ||
is TomlValue.Integer -> append(value.value.toString()) | ||
is TomlValue.LocalDate -> append(value.value.format(DateTimeFormatter.ISO_LOCAL_DATE)) | ||
is TomlValue.LocalDateTime -> append(value.value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) | ||
is TomlValue.LocalTime -> append(value.value.format(DateTimeFormatter.ISO_LOCAL_TIME)) | ||
is TomlValue.OffsetDateTime -> append(value.value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) | ||
is TomlValue.String -> writeValue(value) | ||
} | ||
} | ||
|
||
private fun TomlSerializerState.writeValue(value: TomlValue.List) { | ||
list(value) { | ||
value.elements.fold(true) { first, element -> | ||
if (!first) { | ||
appendListSeparator() | ||
} | ||
writeValue(element) | ||
false | ||
} | ||
} | ||
} | ||
|
||
private fun TomlSerializerState.writeValue(value: TomlValue.Map) { | ||
table { | ||
value.properties.entries.fold(true) { first, it -> | ||
if (!first) { | ||
append(", ") | ||
} | ||
append(encodeKey(it.key), " = ") | ||
writeValue(it.value) | ||
false | ||
} | ||
} | ||
} | ||
|
||
private fun encodePath(path: List<String>) = | ||
path.joinToString(".", transform = ::encodeKey) | ||
|
||
private fun encodeKey(key: String): String = when { | ||
!key.contains(quotesRequiredRegex) -> key | ||
!key.contains('\n') -> "'$key'" | ||
else -> { | ||
val escapedKey = key.replace("\n", "\\n").replace("\"", "\\\"") | ||
"\"$escapedKey\"" | ||
} | ||
} | ||
|
||
private val quotesRequiredRegex = Regex("[^a-zA-Z0-9]") | ||
|
||
private enum class QuoteType(val quotes: String) { | ||
Plain("\""), | ||
Literal("'"), | ||
Multiline("\"\"\""), | ||
MultilineLiteral("'''") | ||
} | ||
|
||
private fun <T> List<T>.removeIf(condition: Boolean, vararg remove: T) = if (condition) { | ||
filter { it !in remove } | ||
} else { | ||
this | ||
} | ||
|
||
private fun TomlSerializerState.writeValue(value: TomlValue.String) { | ||
val invalidChars = value.value.filter { it in invalidTomlChars }.toSet() | ||
if (invalidChars.isNotEmpty()) { | ||
val invalidCharString = invalidChars.joinToString(", ") { "\\u${it.code.toString(16)}" } | ||
throw TomlException.SerializationError( | ||
"string contains characters which are not allowed in a toml document: $invalidCharString", | ||
null | ||
) | ||
} | ||
val eligibleQuoteTypes = QuoteType.values().toList() | ||
.removeIf(value.value.contains("''"), QuoteType.Literal, QuoteType.MultilineLiteral) | ||
.removeIf(value.value.contains("\"\""), QuoteType.Plain, QuoteType.Multiline) | ||
.removeIf(value.value.contains("'"), QuoteType.Literal) | ||
.removeIf(value.value.contains("\""), QuoteType.Plain) | ||
.removeIf(value.value.contains("\n"), QuoteType.Literal, QuoteType.Plain) | ||
val quoteType = eligibleQuoteTypes.firstOrNull() ?: QuoteType.Plain | ||
val text = when (quoteType) { | ||
QuoteType.Plain -> value.value.replace("\"", "\\\"").replace("\n", "\\n") | ||
QuoteType.Multiline -> "\n${value.value}" | ||
QuoteType.Literal -> value.value | ||
QuoteType.MultilineLiteral -> "\n${value.value}" | ||
} | ||
append(quoteType.quotes, text, quoteType.quotes) | ||
} |
Oops, something went wrong.