Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ package org.ekrich.config
*
* <p> Here is an example of creating a `ConfigRenderOptions`:
*
* <pre> ConfigRenderOptions options =
* ConfigRenderOptions.defaults().setComments(false) </pre>
* <pre> val options = ConfigRenderOptions.defaults.setComments(false) </pre>
*/
object ConfigRenderOptions {

Expand All @@ -34,11 +33,19 @@ object ConfigRenderOptions {
def concise = new ConfigRenderOptions(false, false, false, true)
}

case class FormattingOptions(
keepOriginOrder: Boolean = false,
doubleIndent: Boolean = true,
colonAssign: Boolean = false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Contributor

@kastoestoramadus kastoestoramadus Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plus you've covered defaults with tests. Your changes look good

newLineAtEnd: Boolean = true
)

final class ConfigRenderOptions private (
val originComments: Boolean,
val comments: Boolean,
val formatted: Boolean,
val json: Boolean
val json: Boolean,
val formattingOptions: FormattingOptions = FormattingOptions()
) {

/**
Expand Down Expand Up @@ -114,6 +121,28 @@ final class ConfigRenderOptions private (
*/
def getFormatted: Boolean = formatted

/**
* Returns new render options with formatting options set. Formatting is
* dependant on formatted flag.
*
* @param value
* true to enable formatting
* @return
* options with requested setting for formatting
*/
def setFormattingOptions(value: FormattingOptions): ConfigRenderOptions =
if (value == formattingOptions) this
else
new ConfigRenderOptions(originComments, comments, formatted, json, value)

/**
* Returns options used to format the config.
*
* @return
* FormattingOptions
*/
def getFormattingOptions: FormattingOptions = formattingOptions

/**
* Returns options with JSON toggled. JSON means that HOCON extensions
* (omitting commas, quotes for example) won't be used. However, whether to
Expand Down Expand Up @@ -143,10 +172,18 @@ final class ConfigRenderOptions private (
val sb = new StringBuilder("ConfigRenderOptions(")
if (originComments) sb.append("originComments,")
if (comments) sb.append("comments,")
if (formatted) sb.append("formatted,")
if (formatted) {
sb.append("formatted,")
if (formattingOptions.keepOriginOrder) sb.append("keepOriginOrder,")
if (formattingOptions.doubleIndent) sb.append("doubleIndent,")
if (formattingOptions.colonAssign) sb.append("equalsAssign,")
}
if (json) sb.append("json,")
if (sb.charAt(sb.length - 1) == ',') sb.setLength(sb.length - 1)
sb.append(")")
val lastIndex = sb.length - 1
if (sb.charAt(lastIndex) == ',')
sb.setCharAt(lastIndex, ')')
else
sb.append(')')
sb.toString
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ object AbstractConfigValue {
if (options.getFormatted) {
var remaining = indent
while (remaining > 0) {
sb.append(" ")
sb.append(if (options.formattingOptions.doubleIndent) " " else " ")
remaining -= 1
}
}
Expand Down Expand Up @@ -337,7 +337,9 @@ abstract class AbstractConfigValue private[impl] (val _origin: ConfigOrigin)
if (this.isInstanceOf[ConfigObject]) {
if (options.getFormatted) sb.append(' ')
} else {
sb.append("=")
sb.append(
if (options.formattingOptions.colonAssign) ":" else "="
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
*/
package org.ekrich.config.impl

import java.{lang => jl}
import java.lang as jl
import java.io.ObjectStreamException
import java.io.Serializable
import java.{util => ju}
import scala.jdk.CollectionConverters._
import scala.util.control.Breaks._
import java.util as ju
import scala.jdk.CollectionConverters.*
import scala.util.control.Breaks.*
import org.ekrich.config.ConfigException
import org.ekrich.config.ConfigObject
import org.ekrich.config.ConfigOrigin
Expand Down Expand Up @@ -54,7 +54,7 @@ object SimpleConfigObject {

// this is only Serializable to chill out a findbugs warning
@SerialVersionUID(1L)
private object RenderComparator {
private object OrderedRenderComparator {
private def isAllDigits(s: String): Boolean = {
val length = s.length
// empty string doesn't count as a number
Expand All @@ -79,27 +79,51 @@ object SimpleConfigObject {
}

@SerialVersionUID(1L)
final private class RenderComparator
private sealed abstract class RenderComparator
extends ju.Comparator[String]
with Serializable {
def compare(a: String, b: String): Int
}

@SerialVersionUID(1L)
final private class OrderedRenderComparator extends RenderComparator {
// This is supposed to sort numbers before strings,
// and sort the numbers numerically. The point is
// to make objects which are really list-like
// (numeric indices) appear in order.
override def compare(a: String, b: String): Int = {
val aDigits = RenderComparator.isAllDigits(a)
val bDigits = RenderComparator.isAllDigits(b)
val aDigits = OrderedRenderComparator.isAllDigits(a)
val bDigits = OrderedRenderComparator.isAllDigits(b)
if (aDigits && bDigits) Integer.compare(a.toInt, b.toInt)
else if (aDigits) -1
else if (bDigits) 1
else a.compareTo(b)
}
}

@SerialVersionUID(1L)
final private class KeepOriginRenderComparator(
getOriginFor: String => SimpleConfigOrigin
) extends RenderComparator {
override def compare(a: String, b: String): Int = {
val aOrigin = getOriginFor(a)
val bOrigin = getOriginFor(b)

val aFilename = Option(aOrigin.filename).getOrElse("")
val bFilename = Option(bOrigin.filename).getOrElse("")

val compareFiles = aFilename compareTo bFilename

if (compareFiles != 0) compareFiles
else
aOrigin.lineNumber compareTo bOrigin.lineNumber
}
}

private def mapEquals(
a: ju.Map[String, ConfigValue],
b: ju.Map[String, ConfigValue]
): Boolean =
) =
if (a eq b) true
else if (a.keySet != b.keySet) false
else !a.keySet.asScala.exists(key => a.get(key) != b.get(key))
Expand Down Expand Up @@ -493,11 +517,17 @@ final class SimpleConfigObject(
if (options.getFormatted) sb.append('\n')
} else innerIndent = indentVal
var separatorCount = 0

val keys = new ju.ArrayList[String]
keys.addAll(keySet)
ju.Collections.sort(keys, new SimpleConfigObject.RenderComparator)
// val keys: Array[String] = keySet.toArray(new Array[String](size))
// ju.Arrays.sort(keys, new SimpleConfigObject.RenderComparator)
val ordering =
if (options.formattingOptions.keepOriginOrder)
new SimpleConfigObject.KeepOriginRenderComparator(str =>
value.get(str).origin
)
else new SimpleConfigObject.OrderedRenderComparator
ju.Collections.sort(keys, ordering)

for (k <- keys.asScala) {
var v: AbstractConfigValue = null
v = value.get(k)
Expand Down Expand Up @@ -544,7 +574,8 @@ final class SimpleConfigObject(
sb.append("}")
}
}
if (atRoot && options.getFormatted) sb.append('\n')
if (atRoot && options.getFormatted && options.getFormattingOptions.newLineAtEnd)
sb.append('\n')
}

override def get(key: Any): AbstractConfigValue = value.get(key)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package org.ekrich.config.impl

import org.junit.*
import org.ekrich.config.{
ConfigFactory,
ConfigParseOptions,
ConfigRenderOptions,
FormattingOptions
}

class FormattingOptionsTest extends TestUtilsShared {
val parseOptions = ConfigParseOptions.defaults.setAllowMissing(true)
val myDefaultRenderOptions = ConfigRenderOptions.defaults
.setJson(false)
.setOriginComments(false)
.setComments(true)
.setFormatted(true)

def formatHocon(
str: String
)(implicit formattingOptions: FormattingOptions): String =
ConfigFactory
.parseString(str, parseOptions)
.root
.render(myDefaultRenderOptions.setFormattingOptions(formattingOptions))

@Test
def noNewLineAtTheEnd(): Unit = {
implicit val formattingOptions = FormattingOptions(newLineAtEnd = false)
val in = """r {
|}""".stripMargin
val result = formatHocon(in)
val expected = "r {}"
checkEqualObjects(result, expected)
}

@Test
def newLineAtTheEnd(): Unit = {
implicit val formattingOptions = FormattingOptions()
val in = """r {
|}""".stripMargin
val result = formatHocon(in)
val expected = """r {}
|""".stripMargin
checkEqualObjects(result, expected)
}

@Test
def keepOriginOrderOfEntries(): Unit = {
implicit val formattingOptions = FormattingOptions(keepOriginOrder = true)

val in = """r {
| p {
| s: ${r.ss}
| }
| f {
| s=t_f
| n="ALA"
| }
|}""".stripMargin
val result = formatHocon(in)

val expected = """r {
| p {
| s=${r.ss}
| }
| f {
| s="t_f"
| n=ALA
| }
|}
|""".stripMargin
checkEqualObjects(result, expected)
}

@Test
def useTwoSpacesIndentation(): Unit = {
implicit val formattingOptions = FormattingOptions(doubleIndent = false)

val in = """r {
| p {
| d {
| s: ${r.ss}
| }
| }
|}""".stripMargin
val result = formatHocon(in)

val expected = """r {
| p {
| d {
| s=${r.ss}
| }
| }
|}
|""".stripMargin
checkEqualObjects(result, expected)
}

@Test
def useFourSpacesIndentation(): Unit = {
implicit val formattingOptions = FormattingOptions()

val in = """r {
| p {
| d {
| s: ${r.ss}
| }
| }
|}""".stripMargin
val result = formatHocon(in)

val expected = """r {
| p {
| d {
| s=${r.ss}
| }
| }
|}
|""".stripMargin
checkEqualObjects(result, expected)
}

@Test
def useColonAsAssignSign(): Unit = {
implicit val formattingOptions = FormattingOptions(colonAssign = true)

val in = """r {
| s=t_f
| n-m=1
| n:"ALA"
|}""".stripMargin
val result = formatHocon(in)

val expected = """r {
| n:ALA
| "n-m":1
| s:"t_f"
|}
|""".stripMargin
checkEqualObjects(result, expected)
}

@Test
def useEqualsAsAssignSign(): Unit = {
implicit val formattingOptions = FormattingOptions(colonAssign = false)

val in = """r {
| s=t_f
| n-m=1
| n:"ALA"
|}""".stripMargin
val result = formatHocon(in)

val expected = """r {
| n=ALA
| "n-m"=1
| s="t_f"
|}
|""".stripMargin
checkEqualObjects(result, expected)
}
}