All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.maryk.yaml.YamlWriter.kt Maven / Gradle / Ivy

package maryk.yaml

import maryk.json.AbstractJsonLikeWriter
import maryk.json.IllegalJsonOperation
import maryk.json.JsonEmbedType
import maryk.json.JsonEmbedType.ComplexField
import maryk.json.JsonEmbedType.Object
import maryk.json.JsonType.ARRAY_VALUE
import maryk.json.JsonType.COMPLEX_FIELD_NAME_END
import maryk.json.JsonType.COMPLEX_FIELD_NAME_START
import maryk.json.JsonType.END_ARRAY
import maryk.json.JsonType.END_OBJ
import maryk.json.JsonType.FIELD_NAME
import maryk.json.JsonType.OBJ_VALUE
import maryk.json.JsonType.START
import maryk.json.JsonType.START_ARRAY
import maryk.json.JsonType.START_OBJ
import maryk.json.JsonType.TAG

/** A Yaml writer which writes to [writer] */
class YamlWriter(
    private val writer: (String) -> Unit
) : AbstractJsonLikeWriter() {
    private val spacing: String = "  "
    private val arraySpacing: String = "- "
    private val toSanitizeRegex = Regex("[\\[{]+.*|.*[#:\n]+.*")

    private var prefix: String = ""
    private var prefixWasWritten = false
    private var compactStartedAtLevel: Int? = null

    private val prefixToWrite: String
        get() = if (this.prefixWasWritten) {
            this.prefixWasWritten = false
            ""
        } else prefix

    private val lastIsCompact: Boolean
        get() {
            this.compactStartedAtLevel?.let {
                if (this.typeStack.size < it) {
                    this.compactStartedAtLevel = null
                } else return true
            }

            return this.typeStack.lastOrNull()?.isSimple ?: false
        }

    override fun writeStartObject(isCompact: Boolean) {
        if (isCompact || this.lastIsCompact) {
            if (this.lastType == FIELD_NAME
                || this.lastType == TAG
                || this.lastType == COMPLEX_FIELD_NAME_END
            ) {
                writer(" ")
            } else if (this.lastType == END_OBJ) {
                val lastEmbedType = this.typeStack.lastOrNull()
                if (lastEmbedType is JsonEmbedType.Array) {
                    writer(", ")
                }
            }
            writer("{")

            super.writeStartObject(isCompact)

            this.compactStartedAtLevel = this.typeStack.size
        } else {
            val prefixWasWrittenBefore = this.prefixWasWritten
            if (lastType == FIELD_NAME || lastType == TAG) {
                writer("\n")
                this.prefixWasWritten = false
            } else if (lastType == COMPLEX_FIELD_NAME_END) {
                writer(" ")
            }

            val lastEmbedType = this.typeStack.lastOrNull()

            // If starting object within array then add array field
            if (lastEmbedType != null && lastEmbedType is JsonEmbedType.Array && !prefixWasWrittenBefore) {
                writer("$prefixToWrite$arraySpacing")
                this.prefixWasWritten = true
            }

            super.writeStartObject(isCompact)

            if (lastEmbedType != null && lastEmbedType != ComplexField) {
                prefix += spacing
            }
        }
    }

    override fun writeEndObject() {
        if (this.lastIsCompact) {
            writer("}")
            super.writeEndObject()
            if (!this.lastIsCompact) {
                writer("\n")
                this.prefixWasWritten = false
            }
        } else {
            super.writeEndObject()
            if (this.typeStack.isNotEmpty() && this.typeStack.last() !== ComplexField) {
                prefix = prefix.removeSuffix(spacing)
            }
        }
    }

    override fun writeStartArray(isCompact: Boolean) {
        if (!this.lastIsCompact) {
            when (lastType) {
                TAG -> {
                    if (!isCompact) {
                        writer("\n")
                        if (typeStack.last() is JsonEmbedType.Array) {
                            this.prefix += "  "
                        }
                        this.prefixWasWritten = false
                    } else {
                        writer(" ")
                    }
                }
                FIELD_NAME, COMPLEX_FIELD_NAME_END -> {
                    if (!isCompact) {
                        writer("\n")
                        this.prefixWasWritten = false
                    } else {
                        writer(" ")
                    }
                }
                START_ARRAY -> {
                    writer("$prefixToWrite- ")
                    this.prefixWasWritten = true
                    prefix += spacing
                }
                END_ARRAY -> {
                    prefix = prefix.removeSuffix(spacing)
                    writer("$prefixToWrite- ")
                    this.prefixWasWritten = true
                    prefix += spacing
                }
                else -> Unit
            }
        } else if (lastType != START_ARRAY && lastType != FIELD_NAME) {
            writer(",")
        }

        if (isCompact || this.lastIsCompact) {
            writer("[")
            this.compactStartedAtLevel = this.typeStack.size + 1 // is written later so + 1
            this.prefixWasWritten = false
        }

        super.writeStartArray(isCompact)
    }

    override fun writeEndArray() {
        if (this.lastIsCompact) {
            writer("]")
            super.writeEndArray()
            if (!this.lastIsCompact) {
                writer("\n")
                this.prefixWasWritten = false
            }
        } else {
            super.writeEndArray()
            val lastType = if (typeStack.isEmpty()) null else typeStack.last()

            if (lastType == null || (lastType !is Object && lastType !is ComplexField)) {
                prefix = prefix.removeSuffix(spacing)
            }
        }
    }

    /** Writes the field [name] for an object */
    override fun writeFieldName(name: String) {
        val lastType = this.lastType

        if (this.lastIsCompact) {
            if (lastType != START_OBJ) {
                writer(", ")
            }
            writer("$name:")
        } else {
            writer("$prefixToWrite$name:")
        }
        super.writeFieldName(name)
    }

    /** Writes a string [value] including quotes */
    override fun writeString(value: String) = writeValue(value)

    /** Writes a [value] excluding quotes */
    override fun writeValue(value: String) = if (typeStack.isNotEmpty()) {
        val valueToWrite = this.sanitizeValue(value)
        val lastTypeBeforeOperation = this.lastType

        if ((lastTypeBeforeOperation == TAG && value != "") || lastTypeBeforeOperation == COMPLEX_FIELD_NAME_END) {
            writer(" ")
        }

        when (typeStack.last()) {
            is Object -> {
                super.checkObjectValueAllowed()
                if (lastTypeBeforeOperation == FIELD_NAME) {
                    writer(" ")
                }

                if (this.lastIsCompact) {
                    writer(valueToWrite)
                } else {
                    writer("$valueToWrite\n")
                    this.prefixWasWritten = false
                }
            }
            is JsonEmbedType.Array -> {
                super.checkArrayValueAllowed()
                if (this.lastIsCompact) {
                    if (lastTypeBeforeOperation == ARRAY_VALUE) {
                        writer(", ")
                    }
                    writer(valueToWrite)
                } else {
                    if (lastTypeBeforeOperation == TAG) {
                        writer("$valueToWrite\n")
                    } else {
                        writer("$prefixToWrite$arraySpacing$valueToWrite\n")
                    }
                    this.prefixWasWritten = false
                }
            }
            is ComplexField -> {
                throw IllegalJsonOperation("Complex fields cannot contain values directly, start an array or object before adding them")
            }
        }
    } else {
        if (this.lastType == TAG) {
            writer(" ")
        }
        writer(value)
    }

    /** Writes a [tag] to YAML output */
    fun writeTag(tag: String) {
        if (this.lastType == FIELD_NAME || this.lastType == COMPLEX_FIELD_NAME_END) {
            writer(" ")
        }

        val lastTypeBeforeCheck = this.lastType

        // If last type is TAG then write it away with an empty value for it
        if (lastType == TAG) {
            writeValue("")
        }

        checkTypeIsAllowed(
            TAG,
            arrayOf(
                START,
                FIELD_NAME,
                ARRAY_VALUE,
                START_ARRAY,
                END_ARRAY,
                END_OBJ,
                COMPLEX_FIELD_NAME_START,
                COMPLEX_FIELD_NAME_END
            )
        )

        if (!this.lastIsCompact) {
            if (this.typeStack.lastOrNull() is JsonEmbedType.Array) {
                writer("$prefixToWrite$arraySpacing$tag")
                this.prefixWasWritten = true
            } else {
                writer(tag)
            }
        } else {
            if (this.typeStack.isNotEmpty()
                && lastTypeBeforeCheck != START_ARRAY
                && this.typeStack.last() is JsonEmbedType.Array
            ) {
                writer(", $tag")
            } else {
                writer(tag)
            }
        }
    }

    fun writeStartComplexField() {
        checkTypeIsAllowed(
            COMPLEX_FIELD_NAME_START,
            arrayOf(START_OBJ, START_ARRAY, OBJ_VALUE, END_OBJ, END_ARRAY)
        )

        writer("$prefixToWrite? ")
        prefixWasWritten = true

        typeStack.add(ComplexField)

        prefix += spacing
    }

    fun writeEndComplexField() {
        checkTypeIsAllowed(
            COMPLEX_FIELD_NAME_END,
            arrayOf(END_OBJ, END_ARRAY, OBJ_VALUE)
        )

        prefix = prefix.removeSuffix(spacing)

        if (typeStack.isEmpty() || typeStack.last() !== ComplexField) {
            throw IllegalJsonOperation("There is no complex field to close")
        }
        typeStack.removeAt(typeStack.lastIndex)

        writer("$prefixToWrite:")
        this.prefixWasWritten = true
    }

    /** If value contains yaml incompatible values it will be surrounded by quotes */
    private fun sanitizeValue(value: String) =
        if (value.matches(toSanitizeRegex)) {
            "'${value.replace("'", "''")}'"
        } else {
            value
        }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy