From b28a5b16d466c3bbdc6769b737e34991c865f928 Mon Sep 17 00:00:00 2001 From: Anssi Heiska Date: Fri, 14 Jun 2024 22:17:04 +0300 Subject: [PATCH] Added indent with indicators to yaml-v12 (#406) * Added indentWithIndicator option * Deprecate Printer(.Config) and introduce PrinterBuilder * Moved PrinterTest to use PrinterBuilder * Added circe-yaml PrinterBuilder that uses circe-yaml-common. -Also support NonPrintableStyle * Added file headers and ran scalafmt * Run scalafix * formatting * Make sure we have correct deprecation version * Fixed issues found in review. --------- Co-authored-by: Anssi Heiska Co-authored-by: Erlend Hamnaberg --- .../scala/io/circe/yaml/common/Printer.scala | 5 + .../scala/io/circe/yaml/v12/Printer.scala | 8 +- .../io/circe/yaml/v12/PrinterBuilder.scala | 161 ++++++++++++++++ .../scala/io/circe/yaml/v12/PrinterImpl.scala | 2 +- .../io/circe/yaml/v12/PrinterTests.scala | 64 ++++-- .../scala/io/circe/yaml/PrinterBuilder.scala | 182 ++++++++++++++++++ .../scala/io/circe/yaml/PrinterImpl.scala | 123 ++++++++++++ .../io/circe/yaml/PrinterBuilderTests.scala | 171 ++++++++++++++++ 8 files changed, 699 insertions(+), 17 deletions(-) create mode 100644 circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterBuilder.scala create mode 100644 circe-yaml/src/main/scala/io/circe/yaml/PrinterBuilder.scala create mode 100644 circe-yaml/src/main/scala/io/circe/yaml/PrinterImpl.scala create mode 100644 circe-yaml/src/test/scala/io/circe/yaml/PrinterBuilderTests.scala diff --git a/circe-yaml-common/src/main/scala/io/circe/yaml/common/Printer.scala b/circe-yaml-common/src/main/scala/io/circe/yaml/common/Printer.scala index b7c058bb..b3728f79 100644 --- a/circe-yaml-common/src/main/scala/io/circe/yaml/common/Printer.scala +++ b/circe-yaml-common/src/main/scala/io/circe/yaml/common/Printer.scala @@ -46,4 +46,9 @@ object Printer { case object Folded extends StringStyle } + sealed trait NonPrintableStyle + object NonPrintableStyle { + case object Binary extends NonPrintableStyle + case object Escape extends NonPrintableStyle + } } diff --git a/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/Printer.scala b/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/Printer.scala index f4b16756..6a4115c6 100644 --- a/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/Printer.scala +++ b/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/Printer.scala @@ -23,6 +23,7 @@ import org.snakeyaml.engine.v2.api.DumpSettings import scala.collection.JavaConverters._ object Printer { + @deprecated("Use Printer.builder instead", since = "1.15.2") final case class Config( preserveOrder: Boolean = false, dropNullKeys: Boolean = false, @@ -39,6 +40,7 @@ object Printer { explicitEnd: Boolean = false ) + @deprecated("Use Printer.builder instead", since = "1.15.2") def make(config: Config = Config()): common.Printer = { import config._ new PrinterImpl( @@ -68,7 +70,7 @@ object Printer { ) } - lazy val spaces2: common.Printer = make() - lazy val spaces4: common.Printer = make(Config(indent = 4)) - + def builder: PrinterBuilder = PrinterBuilder() + lazy val spaces2: common.Printer = builder.withIndent(2).build() + lazy val spaces4: common.Printer = builder.withIndent(4).build() } diff --git a/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterBuilder.scala b/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterBuilder.scala new file mode 100644 index 00000000..53d51c55 --- /dev/null +++ b/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterBuilder.scala @@ -0,0 +1,161 @@ +/* + * Copyright 2016 circe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.circe.yaml.v12 + +import io.circe.yaml.common +import io.circe.yaml.common.Printer._ +import org.snakeyaml.engine.v2.api.DumpSettings +import org.snakeyaml.engine.v2.common.{ NonPrintableStyle => SnakeNonPrintableStyle } + +import scala.collection.JavaConverters._ + +final class PrinterBuilder private ( + preserveOrder: Boolean = false, + dropNullKeys: Boolean = false, + indent: Int = 2, + maxScalarWidth: Int = 80, + splitLines: Boolean = true, + indicatorIndent: Int = 0, + indentWithIndicator: Boolean = false, + tags: Map[String, String] = Map.empty, + sequenceStyle: FlowStyle = FlowStyle.Block, + mappingStyle: FlowStyle = FlowStyle.Block, + stringStyle: StringStyle = StringStyle.Plain, + lineBreak: LineBreak = LineBreak.Unix, + explicitStart: Boolean = false, + explicitEnd: Boolean = false, + nonPrintableStyle: NonPrintableStyle = NonPrintableStyle.Escape +) { + + private def copy( + preserveOrder: Boolean = this.preserveOrder, + dropNullKeys: Boolean = this.dropNullKeys, + indent: Int = this.indent, + maxScalarWidth: Int = this.maxScalarWidth, + splitLines: Boolean = this.splitLines, + indicatorIndent: Int = this.indicatorIndent, + indentWithIndicator: Boolean = this.indentWithIndicator, + tags: Map[String, String] = this.tags, + sequenceStyle: FlowStyle = this.sequenceStyle, + mappingStyle: FlowStyle = this.mappingStyle, + stringStyle: StringStyle = this.stringStyle, + lineBreak: LineBreak = this.lineBreak, + explicitStart: Boolean = this.explicitStart, + explicitEnd: Boolean = this.explicitEnd, + nonPrintableStyle: NonPrintableStyle = this.nonPrintableStyle + ): PrinterBuilder = + new PrinterBuilder( + preserveOrder = preserveOrder, + dropNullKeys = dropNullKeys, + indent = indent, + maxScalarWidth = maxScalarWidth, + splitLines = splitLines, + indicatorIndent = indicatorIndent, + indentWithIndicator = indentWithIndicator, + tags = tags, + sequenceStyle = sequenceStyle, + mappingStyle = mappingStyle, + stringStyle = stringStyle, + lineBreak = lineBreak, + explicitStart = explicitStart, + explicitEnd = explicitEnd, + nonPrintableStyle = nonPrintableStyle + ) + + def withPreserveOrder(preserveOrder: Boolean): PrinterBuilder = + copy(preserveOrder = preserveOrder) + + def withDropNullKeys(dropNullKeys: Boolean): PrinterBuilder = + copy(dropNullKeys = dropNullKeys) + + def withIndent(indent: Int): PrinterBuilder = + copy(indent = indent) + + def withMaxScalarWidth(maxScalarWidth: Int): PrinterBuilder = + copy(maxScalarWidth = maxScalarWidth) + + def withSplitLines(splitLines: Boolean): PrinterBuilder = + copy(splitLines = splitLines) + + def withIndicatorIndent(indicatorIndent: Int): PrinterBuilder = + copy(indicatorIndent = indicatorIndent) + + def withIndentWithIndicator(indentWithIndicator: Boolean): PrinterBuilder = + copy(indentWithIndicator = indentWithIndicator) + + def withTags(tags: Map[String, String]): PrinterBuilder = + copy(tags = tags) + + def withSequenceStyle(sequenceStyle: common.Printer.FlowStyle): PrinterBuilder = + copy(sequenceStyle = sequenceStyle) + + def withMappingStyle(mappingStyle: common.Printer.FlowStyle): PrinterBuilder = + copy(mappingStyle = mappingStyle) + + def withStringStyle(stringStyle: common.Printer.StringStyle): PrinterBuilder = + copy(stringStyle = stringStyle) + + def withLineBreak(lineBreak: common.Printer.LineBreak): PrinterBuilder = + copy(lineBreak = lineBreak) + + def withExplicitStart(explicitStart: Boolean): PrinterBuilder = + copy(explicitStart = explicitStart) + + def withExplicitEnd(explicitEnd: Boolean): PrinterBuilder = + copy(explicitEnd = explicitEnd) + + def withNonPrintableStyle(nonPrintableStyle: NonPrintableStyle): PrinterBuilder = + copy(nonPrintableStyle = nonPrintableStyle) + + def build(): common.Printer = + new PrinterImpl( + stringStyle, + preserveOrder, + dropNullKeys, + mappingStyle, + sequenceStyle, + DumpSettings + .builder() + .setIndent(indent) + .setWidth(maxScalarWidth) + .setSplitLines(splitLines) + .setIndicatorIndent(indicatorIndent) + .setIndentWithIndicator(indentWithIndicator) + .setTagDirective(tags.asJava) + .setDefaultScalarStyle(stringStyle.toScalarStyle) + .setExplicitStart(explicitStart) + .setExplicitEnd(explicitEnd) + .setBestLineBreak { + lineBreak match { + case LineBreak.Unix => "\n" + case LineBreak.Windows => "\r\n" + case LineBreak.Mac => "\r" + } + } + .setNonPrintableStyle { + nonPrintableStyle match { + case NonPrintableStyle.Binary => SnakeNonPrintableStyle.BINARY + case NonPrintableStyle.Escape => SnakeNonPrintableStyle.ESCAPE + } + } + .build() + ) +} + +object PrinterBuilder { + def apply(): PrinterBuilder = new PrinterBuilder() +} diff --git a/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterImpl.scala b/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterImpl.scala index 375c01a9..42bdeece 100644 --- a/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterImpl.scala +++ b/circe-yaml-v12/src/main/scala/io/circe/yaml/v12/PrinterImpl.scala @@ -30,7 +30,7 @@ import org.snakeyaml.engine.v2.serializer.Serializer import java.io.StringWriter import scala.collection.JavaConverters._ -class PrinterImpl( +private class PrinterImpl( stringStyle: StringStyle, preserveOrder: Boolean, dropNullKeys: Boolean, diff --git a/circe-yaml-v12/src/test/scala/io/circe/yaml/v12/PrinterTests.scala b/circe-yaml-v12/src/test/scala/io/circe/yaml/v12/PrinterTests.scala index e35fb7ec..7fd232d9 100644 --- a/circe-yaml-v12/src/test/scala/io/circe/yaml/v12/PrinterTests.scala +++ b/circe-yaml-v12/src/test/scala/io/circe/yaml/v12/PrinterTests.scala @@ -29,7 +29,7 @@ class PrinterTests extends AnyFreeSpec with Matchers { val json = Json.obj("foo" -> Json.arr((0 until 3).map(_.toString).map(Json.fromString): _*)) "Block" in { - val printer = Printer.make(Printer.Config(sequenceStyle = FlowStyle.Block, mappingStyle = FlowStyle.Block)) + val printer = Printer.builder.withSequenceStyle(FlowStyle.Block).withMappingStyle(FlowStyle.Block).build() printer.pretty(json) shouldEqual """foo: |- '0' @@ -39,7 +39,7 @@ class PrinterTests extends AnyFreeSpec with Matchers { } "Flow" in { - val printer = Printer.make(Printer.Config(sequenceStyle = FlowStyle.Flow, mappingStyle = FlowStyle.Flow)) + val printer = Printer.builder.withSequenceStyle(FlowStyle.Block).withMappingStyle(FlowStyle.Flow).build() printer.pretty(json) shouldEqual """{foo: ['0', '1', '2']} |""".stripMargin @@ -50,7 +50,7 @@ class PrinterTests extends AnyFreeSpec with Matchers { val kvPairs = Seq("d" -> 4, "a" -> 1, "b" -> 2, "c" -> 3) val json = Json.obj(kvPairs.map { case (k, v) => k -> Json.fromInt(v) }: _*) "true" in { - val printer = Printer.make(Printer.Config(preserveOrder = true)) + val printer = Printer.builder.withPreserveOrder(true).build() printer.pretty(json) shouldEqual """d: 4 |a: 1 @@ -68,28 +68,28 @@ class PrinterTests extends AnyFreeSpec with Matchers { val json = Json.obj("foo" -> Json.fromString(foosPlain)) "Plain" in { - val printer = Printer.make(Printer.Config(splitLines = false, stringStyle = StringStyle.Plain)) + val printer = Printer.builder.withSplitLines(false).withStringStyle(StringStyle.Plain).build() printer.pretty(json) shouldEqual s"""foo: $foosPlain |""".stripMargin } "Double quoted" in { - val printer = Printer.make(Printer.Config(stringStyle = StringStyle.DoubleQuoted)) + val printer = Printer.builder.withStringStyle(StringStyle.DoubleQuoted).build() printer.pretty(json) shouldEqual s"""foo: "${foosSplit.mkString("\\\n \\ ")}" |""".stripMargin } "Single quoted" in { - val printer = Printer.make(Printer.Config(stringStyle = StringStyle.SingleQuoted)) + val printer = Printer.builder.withStringStyle(StringStyle.SingleQuoted).build() printer.pretty(json) shouldEqual s"""foo: '${foosSplit.mkString("\n ")}' |""".stripMargin } "Folded" in { - val printer = Printer.make(Printer.Config(stringStyle = StringStyle.Folded)) + val printer = Printer.builder.withStringStyle(StringStyle.Folded).build() printer.pretty(json) shouldEqual s"""foo: >- | $foosFolded @@ -97,7 +97,7 @@ class PrinterTests extends AnyFreeSpec with Matchers { } "Literal" in { - val printer = Printer.make(Printer.Config(stringStyle = StringStyle.Literal)) + val printer = Printer.builder.withStringStyle(StringStyle.Literal).build() printer.pretty(json) shouldEqual s"""foo: |- | $foosPlain @@ -108,7 +108,7 @@ class PrinterTests extends AnyFreeSpec with Matchers { "Plain with newlines" in { val json = Json.obj("foo" -> Json.fromString("abc\nxyz\n")) - val printer = Printer.make(Printer.Config(stringStyle = StringStyle.Plain)) + val printer = Printer.builder.withStringStyle(StringStyle.Plain).build() printer.pretty(json) shouldEqual s"""foo: | | abc @@ -118,7 +118,8 @@ class PrinterTests extends AnyFreeSpec with Matchers { "Drop null keys" in { val json = Json.obj("nullField" -> Json.Null, "nonNullField" -> Json.fromString("foo")) - Printer.make(Printer.Config(dropNullKeys = true)).pretty(json) shouldEqual "nonNullField: foo\n" + val printer = Printer.builder.withDropNullKeys(true).build() + printer.pretty(json) shouldEqual "nonNullField: foo\n" } "Root integer" in { @@ -140,19 +141,56 @@ class PrinterTests extends AnyFreeSpec with Matchers { val json = Json.arr(Json.fromString("foo"), Json.fromString("bar")) "Unix" in { - Printer.make(Printer.Config(lineBreak = LineBreak.Unix)).pretty(json) shouldEqual + Printer.builder.withLineBreak(LineBreak.Unix).build().pretty(json) shouldEqual "- foo\n- bar\n" } "Windows" in { - Printer.make(Printer.Config(lineBreak = LineBreak.Windows)).pretty(json) shouldEqual + Printer.builder.withLineBreak(LineBreak.Windows).build().pretty(json) shouldEqual "- foo\r\n- bar\r\n" } "Mac" in { - Printer.make(Printer.Config(lineBreak = LineBreak.Mac)).pretty(json) shouldEqual + Printer.builder.withLineBreak(LineBreak.Mac).build().pretty(json) shouldEqual "- foo\r- bar\r" } } + "Indicator indent" - { + val firstEl = Json.obj("a" -> Json.fromString("b"), "c" -> Json.fromString("d")) + val secondEl = Json.obj("aa" -> Json.fromString("bb"), "cc" -> Json.fromString("dd")) + val json = Json.obj("root" -> Json.arr(firstEl, secondEl)) + + "Default" in { + Printer.spaces2.pretty(json) shouldEqual + """root: + |- a: b + | c: d + |- aa: bb + | cc: dd + |""".stripMargin + } + + "Indent without indentWithIndicator" in { + Printer.builder.withIndicatorIndent(2).build().pretty(json) shouldEqual + """root: + | - + | a: b + | c: d + | - + | aa: bb + | cc: dd + |""".stripMargin + } + + "Indent with indentWithIndicator" in { + Printer.builder.withIndentWithIndicator(true).withIndicatorIndent(2).build().pretty(json) shouldEqual + """root: + | - a: b + | c: d + | - aa: bb + | cc: dd + |""".stripMargin + } + } } diff --git a/circe-yaml/src/main/scala/io/circe/yaml/PrinterBuilder.scala b/circe-yaml/src/main/scala/io/circe/yaml/PrinterBuilder.scala new file mode 100644 index 00000000..1ee0826d --- /dev/null +++ b/circe-yaml/src/main/scala/io/circe/yaml/PrinterBuilder.scala @@ -0,0 +1,182 @@ +/* + * Copyright 2016 circe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.circe.yaml + +import io.circe.yaml.Printer.YamlVersion +import io.circe.yaml.common.Printer._ +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.DumperOptions.ScalarStyle +import org.yaml.snakeyaml.DumperOptions.{ NonPrintableStyle => SnakeNonPrintableStyle } + +import scala.collection.JavaConverters._ + +final class PrinterBuilder private ( + preserveOrder: Boolean = false, + dropNullKeys: Boolean = false, + indent: Int = 2, + maxScalarWidth: Int = 80, + splitLines: Boolean = true, + indicatorIndent: Int = 0, + indentWithIndicator: Boolean = false, + tags: Map[String, String] = Map.empty, + sequenceStyle: FlowStyle = FlowStyle.Block, + mappingStyle: FlowStyle = FlowStyle.Block, + stringStyle: StringStyle = StringStyle.Plain, + lineBreak: LineBreak = LineBreak.Unix, + explicitStart: Boolean = false, + explicitEnd: Boolean = false, + nonPrintableStyle: NonPrintableStyle = NonPrintableStyle.Escape, + yamlVersion: YamlVersion = YamlVersion.Auto +) { + private def copy( + preserveOrder: Boolean = this.preserveOrder, + dropNullKeys: Boolean = this.dropNullKeys, + indent: Int = this.indent, + maxScalarWidth: Int = this.maxScalarWidth, + splitLines: Boolean = this.splitLines, + indicatorIndent: Int = this.indicatorIndent, + indentWithIndicator: Boolean = this.indentWithIndicator, + tags: Map[String, String] = this.tags, + sequenceStyle: FlowStyle = this.sequenceStyle, + mappingStyle: FlowStyle = this.mappingStyle, + stringStyle: StringStyle = this.stringStyle, + lineBreak: LineBreak = this.lineBreak, + explicitStart: Boolean = this.explicitStart, + explicitEnd: Boolean = this.explicitEnd, + nonPrintableStyle: NonPrintableStyle = this.nonPrintableStyle, + yamlVersion: YamlVersion = this.yamlVersion + ): PrinterBuilder = + new PrinterBuilder( + preserveOrder = preserveOrder, + dropNullKeys = dropNullKeys, + indent = indent, + maxScalarWidth = maxScalarWidth, + splitLines = splitLines, + indicatorIndent = indicatorIndent, + indentWithIndicator = indentWithIndicator, + tags = tags, + sequenceStyle = sequenceStyle, + mappingStyle = mappingStyle, + stringStyle = stringStyle, + lineBreak = lineBreak, + explicitStart = explicitStart, + explicitEnd = explicitEnd, + nonPrintableStyle = nonPrintableStyle, + yamlVersion = yamlVersion + ) + + def withPreserveOrder(preserveOrder: Boolean): PrinterBuilder = + copy(preserveOrder = preserveOrder) + + def withDropNullKeys(dropNullKeys: Boolean): PrinterBuilder = + copy(dropNullKeys = dropNullKeys) + + def withIndent(indent: Int): PrinterBuilder = + copy(indent = indent) + + def withMaxScalarWidth(maxScalarWidth: Int): PrinterBuilder = + copy(maxScalarWidth = maxScalarWidth) + + def withSplitLines(splitLines: Boolean): PrinterBuilder = + copy(splitLines = splitLines) + + def withIndicatorIndent(indicatorIndent: Int): PrinterBuilder = + copy(indicatorIndent = indicatorIndent) + + def withIndentWithIndicator(indentWithIndicator: Boolean): PrinterBuilder = + copy(indentWithIndicator = indentWithIndicator) + + def withTags(tags: Map[String, String]): PrinterBuilder = + copy(tags = tags) + + def withSequenceStyle(sequenceStyle: common.Printer.FlowStyle): PrinterBuilder = + copy(sequenceStyle = sequenceStyle) + + def withMappingStyle(mappingStyle: common.Printer.FlowStyle): PrinterBuilder = + copy(mappingStyle = mappingStyle) + + def withStringStyle(stringStyle: common.Printer.StringStyle): PrinterBuilder = + copy(stringStyle = stringStyle) + + def withLineBreak(lineBreak: common.Printer.LineBreak): PrinterBuilder = + copy(lineBreak = lineBreak) + + def withExplicitStart(explicitStart: Boolean): PrinterBuilder = + copy(explicitStart = explicitStart) + + def withExplicitEnd(explicitEnd: Boolean): PrinterBuilder = + copy(explicitEnd = explicitEnd) + + def withNonPrintableStyle(nonPrintableStyle: NonPrintableStyle): PrinterBuilder = + copy(nonPrintableStyle = nonPrintableStyle) + + def withYamlVersion(yamlVersion: YamlVersion): PrinterBuilder = + copy(yamlVersion = yamlVersion) + + def build(): common.Printer = { + import PrinterBuilder.* + val options = new DumperOptions() + options.setIndent(indent) + options.setWidth(maxScalarWidth) + options.setSplitLines(splitLines) + options.setIndicatorIndent(indicatorIndent) + options.setIndentWithIndicator(indentWithIndicator) + options.setTags(tags.asJava) + options.setDefaultScalarStyle(stringStyle.toScalarStyle) + options.setExplicitStart(explicitStart) + options.setExplicitEnd(explicitEnd) + options.setLineBreak(lineBreak match { + case LineBreak.Unix => org.yaml.snakeyaml.DumperOptions.LineBreak.UNIX + case LineBreak.Windows => org.yaml.snakeyaml.DumperOptions.LineBreak.WIN + case LineBreak.Mac => org.yaml.snakeyaml.DumperOptions.LineBreak.MAC + }) + options.setNonPrintableStyle(nonPrintableStyle match { + case NonPrintableStyle.Binary => SnakeNonPrintableStyle.BINARY + case NonPrintableStyle.Escape => SnakeNonPrintableStyle.ESCAPE + }) + options.setVersion(yamlVersion match { + case YamlVersion.Auto => null + case YamlVersion.Yaml1_0 => DumperOptions.Version.V1_0 + case YamlVersion.Yaml1_1 => DumperOptions.Version.V1_1 + }) + + new PrinterImpl( + stringStyle, + preserveOrder, + dropNullKeys, + mappingStyle, + sequenceStyle, + options + ) + } +} + +object PrinterBuilder { + def apply(): PrinterBuilder = new PrinterBuilder() + def spaces2: common.Printer = new PrinterBuilder().build() + def spaces4: common.Printer = new PrinterBuilder(indent = 4).build() + + implicit class SnakeStringStyle(stringStyle: StringStyle) { + def toScalarStyle: ScalarStyle = stringStyle match { + case StringStyle.Plain => ScalarStyle.PLAIN + case StringStyle.DoubleQuoted => ScalarStyle.DOUBLE_QUOTED + case StringStyle.SingleQuoted => ScalarStyle.SINGLE_QUOTED + case StringStyle.Literal => ScalarStyle.LITERAL + case StringStyle.Folded => ScalarStyle.FOLDED + } + } +} diff --git a/circe-yaml/src/main/scala/io/circe/yaml/PrinterImpl.scala b/circe-yaml/src/main/scala/io/circe/yaml/PrinterImpl.scala new file mode 100644 index 00000000..13f70acc --- /dev/null +++ b/circe-yaml/src/main/scala/io/circe/yaml/PrinterImpl.scala @@ -0,0 +1,123 @@ +/* + * Copyright 2016 circe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.circe.yaml + +import io.circe.Json +import io.circe.JsonNumber +import io.circe.JsonObject +import io.circe.yaml.PrinterBuilder.SnakeStringStyle +import io.circe.yaml.common.Printer._ +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.emitter.Emitter +import org.yaml.snakeyaml.nodes.MappingNode +import org.yaml.snakeyaml.nodes.Node +import org.yaml.snakeyaml.nodes.NodeTuple +import org.yaml.snakeyaml.nodes.ScalarNode +import org.yaml.snakeyaml.nodes.SequenceNode +import org.yaml.snakeyaml.nodes.Tag +import org.yaml.snakeyaml.resolver.Resolver +import org.yaml.snakeyaml.serializer.Serializer + +import java.io.StringWriter +import scala.collection.JavaConverters._ + +private class PrinterImpl( + stringStyle: StringStyle, + preserveOrder: Boolean, + dropNullKeys: Boolean, + mappingStyle: FlowStyle, + sequenceStyle: FlowStyle, + options: DumperOptions +) extends io.circe.yaml.common.Printer { + + import PrinterImpl.* + + def pretty(json: Json): String = { + val rootTag = yamlTag(json) + val writer = new StringWriter() + val serializer = new Serializer(new Emitter(writer, options), new Resolver, options, rootTag) + serializer.open() + serializer.serialize(jsonToYaml(json)) + serializer.close() + writer.toString + } + + private def isBad(s: String): Boolean = s.indexOf('\u0085') >= 0 || s.indexOf('\ufeff') >= 0 + + private def hasNewline(s: String): Boolean = s.indexOf('\n') >= 0 + + private def scalarStyle(value: String): DumperOptions.ScalarStyle = + if (isBad(value)) DumperOptions.ScalarStyle.DOUBLE_QUOTED else DumperOptions.ScalarStyle.PLAIN + + private def stringScalarStyle(value: String): DumperOptions.ScalarStyle = + if (isBad(value)) DumperOptions.ScalarStyle.DOUBLE_QUOTED + else if (stringStyle == StringStyle.Plain && hasNewline(value)) DumperOptions.ScalarStyle.LITERAL + else stringStyle.toScalarStyle + + private def scalarNode(tag: Tag, value: String) = new ScalarNode(tag, value, null, null, scalarStyle(value)) + + private def stringNode(value: String) = new ScalarNode(Tag.STR, value, null, null, stringScalarStyle(value)) + + private def keyNode(value: String) = new ScalarNode(Tag.STR, value, null, null, scalarStyle(value)) + + private def jsonToYaml(json: Json): Node = { + + def convertObject(obj: JsonObject): MappingNode = { + val fields = if (preserveOrder) obj.keys else obj.keys.toSet + val m = obj.toMap + val childNodes = fields.flatMap { key => + val value = m(key) + if (!dropNullKeys || !value.isNull) Some(new NodeTuple(keyNode(key), jsonToYaml(value))) + else None + } + new MappingNode( + Tag.MAP, + childNodes.toList.asJava, + if (mappingStyle == FlowStyle.Flow) DumperOptions.FlowStyle.FLOW else DumperOptions.FlowStyle.BLOCK + ) + } + + json.fold( + scalarNode(Tag.NULL, "null"), + bool => scalarNode(Tag.BOOL, bool.toString), + number => scalarNode(numberTag(number), number.toString), + str => stringNode(str), + arr => + new SequenceNode( + Tag.SEQ, + arr.map(jsonToYaml).asJava, + if (sequenceStyle == FlowStyle.Flow) DumperOptions.FlowStyle.FLOW else DumperOptions.FlowStyle.BLOCK + ), + obj => convertObject(obj) + ) + } + +} + +object PrinterImpl { + private def numberTag(number: JsonNumber): Tag = + if (number.toString.contains(".")) Tag.FLOAT else Tag.INT + + private def yamlTag(json: Json): Tag = json.fold( + Tag.NULL, + _ => Tag.BOOL, + number => numberTag(number), + _ => Tag.STR, + _ => Tag.SEQ, + _ => Tag.MAP + ) +} diff --git a/circe-yaml/src/test/scala/io/circe/yaml/PrinterBuilderTests.scala b/circe-yaml/src/test/scala/io/circe/yaml/PrinterBuilderTests.scala new file mode 100644 index 00000000..655e5c14 --- /dev/null +++ b/circe-yaml/src/test/scala/io/circe/yaml/PrinterBuilderTests.scala @@ -0,0 +1,171 @@ +/* + * Copyright 2016 circe + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.circe.yaml + +import io.circe.Json +import io.circe.yaml.Printer.YamlVersion +import io.circe.yaml.common.Printer.FlowStyle +import io.circe.yaml.common.Printer.LineBreak +import io.circe.yaml.common.Printer.StringStyle +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +class PrinterBuilderTests extends AnyFreeSpec with Matchers { + + "Flow style" - { + val json = Json.obj("foo" -> Json.arr((0 until 3).map(_.toString).map(Json.fromString): _*)) + + "Block" in { + val printer = PrinterBuilder().withSequenceStyle(FlowStyle.Block).withMappingStyle(FlowStyle.Block).build() + printer.pretty(json) shouldEqual + """foo: + |- '0' + |- '1' + |- '2' + |""".stripMargin + } + + "Flow" in { + val printer = PrinterBuilder().withSequenceStyle(FlowStyle.Flow).withMappingStyle(FlowStyle.Flow).build() + printer.pretty(json) shouldEqual + """{foo: ['0', '1', '2']} + |""".stripMargin + } + } + + "Preserves order" - { + val kvPairs = Seq("d" -> 4, "a" -> 1, "b" -> 2, "c" -> 3) + val json = Json.obj(kvPairs.map { case (k, v) => k -> Json.fromInt(v) }: _*) + "true" in { + val printer = PrinterBuilder().withPreserveOrder(true).build() + printer.pretty(json) shouldEqual + """d: 4 + |a: 1 + |b: 2 + |c: 3 + |""".stripMargin + } + } + + "Scalar style" - { + val foos = Seq.fill(40)("foo") + val foosSplit = Seq(foos.take(19), foos.slice(19, 39), foos.slice(39, 40)).map(_.mkString(" ")) + val foosPlain = foos.mkString(" ") + val foosFolded = Seq(foos.take(20), foos.slice(20, 40)).map(_.mkString(" ")).mkString("\n ") + val json = Json.obj("foo" -> Json.fromString(foosPlain)) + + "Plain" in { + val printer = PrinterBuilder().withSplitLines(false).withStringStyle(StringStyle.Plain).build() + printer.pretty(json) shouldEqual + s"""foo: $foosPlain + |""".stripMargin + } + + "Double quoted" in { + val printer = PrinterBuilder().withStringStyle(StringStyle.DoubleQuoted).build() + printer.pretty(json) shouldEqual + s"""foo: "${foosSplit.mkString("\\\n \\ ")}" + |""".stripMargin + } + + "Single quoted" in { + val printer = PrinterBuilder().withStringStyle(StringStyle.SingleQuoted).build() + printer.pretty(json) shouldEqual + s"""foo: '${foosSplit.mkString("\n ")}' + |""".stripMargin + } + + "Folded" in { + val printer = PrinterBuilder().withStringStyle(StringStyle.Folded).build() + printer.pretty(json) shouldEqual + s"""foo: >- + | $foosFolded + |""".stripMargin + } + + "Literal" in { + val printer = PrinterBuilder().withStringStyle(StringStyle.Literal).build() + printer.pretty(json) shouldEqual + s"""foo: |- + | $foosPlain + |""".stripMargin + } + + } + + "Plain with newlines" in { + val json = Json.obj("foo" -> Json.fromString("abc\nxyz\n")) + val printer = PrinterBuilder().withStringStyle(StringStyle.Plain).build() + printer.pretty(json) shouldEqual + s"""foo: | + | abc + | xyz + |""".stripMargin + } + + "Drop null keys" in { + val json = Json.obj("nullField" -> Json.Null, "nonNullField" -> Json.fromString("foo")) + PrinterBuilder().withDropNullKeys(true).build().pretty(json) shouldEqual "nonNullField: foo\n" + } + + "Root integer" in { + val json = Json.fromInt(10) + PrinterBuilder.spaces2.pretty(json) shouldEqual "10\n" + } + + "Root float" in { + val json = Json.fromDoubleOrNull(22.22) + PrinterBuilder.spaces2.pretty(json) shouldEqual "22.22\n" + } + + "Root float without decimal part" in { + val json = Json.fromDoubleOrNull(22.0) + PrinterBuilder.spaces2.pretty(json) shouldEqual "22.0\n" + } + + "Version" in { + val json = Json.fromString("foo") + PrinterBuilder().withYamlVersion(YamlVersion.Yaml1_1).build().pretty(json) shouldEqual + """%YAML 1.1 + |--- foo + |""".stripMargin + PrinterBuilder().withYamlVersion(YamlVersion.Yaml1_0).build().pretty(json) shouldEqual + """%YAML 1.0 + |--- foo + |""".stripMargin + } + + "Line break" - { + val json = Json.arr(Json.fromString("foo"), Json.fromString("bar")) + + "Unix" in { + PrinterBuilder().withLineBreak(LineBreak.Unix).build().pretty(json) shouldEqual + "- foo\n- bar\n" + } + + "Windows" in { + PrinterBuilder().withLineBreak(LineBreak.Windows).build().pretty(json) shouldEqual + "- foo\r\n- bar\r\n" + } + + "Mac" in { + PrinterBuilder().withLineBreak(LineBreak.Mac).build().pretty(json) shouldEqual + "- foo\r- bar\r" + } + } + +}