diff --git a/json/src/main/scala/fs2/data/json/tokens.scala b/json/src/main/scala/fs2/data/json/tokens.scala index 42a9006b..15c92dc6 100644 --- a/json/src/main/scala/fs2/data/json/tokens.scala +++ b/json/src/main/scala/fs2/data/json/tokens.scala @@ -18,9 +18,15 @@ package fs2 package data package json +import fs2.Pure +import fs2.data.text.render.{DocEvent, Renderable, Renderer} + +import scala.annotation.switch + sealed abstract class Token(val kind: String) { def jsonRepr: String } + object Token { case object StartObject extends Token("object") { @@ -58,4 +64,104 @@ object Token { def jsonRepr: String = s""""$value"""" } + implicit object renderable extends Renderable[Token] { + + private val startObject = + Stream.emits(DocEvent.GroupBegin :: DocEvent.Text("{") :: DocEvent.IndentBegin :: DocEvent.LineBreak :: Nil) + + private val endObject = + Stream.emits(DocEvent.IndentEnd :: DocEvent.LineBreak :: DocEvent.Text("}") :: DocEvent.GroupEnd :: Nil) + + private val startArray = + Stream.emits(DocEvent.GroupBegin :: DocEvent.Text("[") :: DocEvent.IndentBegin :: DocEvent.LineBreak :: Nil) + + private val endArray = + Stream.emits(DocEvent.IndentEnd :: DocEvent.LineBreak :: DocEvent.Text("]") :: DocEvent.GroupEnd :: Nil) + + private val objectSep = + Stream.emits( + DocEvent.Text(",") :: DocEvent.IndentEnd :: DocEvent.GroupEnd :: DocEvent.GroupEnd :: DocEvent.Line :: Nil) + + private val arraySep = + Stream.emits(DocEvent.Text(",") :: DocEvent.GroupEnd :: DocEvent.Line :: Nil) + + private val nullValue = + Stream.emit(DocEvent.Text("null")) + + private val trueValue = + Stream.emit(DocEvent.Text("true")) + + private val falseValue = + Stream.emit(DocEvent.Text("false")) + + private final val FirstObjectKey = 0 + private final val ObjectKey = 1 + private final val ObjectValue = 2 + private final val FirstArrayValue = 3 + private final val ArrayValue = 4 + + override def newRenderer(): Renderer[Token] = new Renderer[Token] { + + // the current stack of states, helping to deal with comma and indentation + // states are described right above + private[this] var states = List.empty[Int] + + private def separator(): Stream[Pure, DocEvent] = + states match { + case Nil => + Stream.empty + case state :: rest => + (state: @switch) match { + case FirstObjectKey => + states = ObjectValue :: rest + Stream.empty + case ObjectKey => + states = ObjectValue :: rest + objectSep + case ObjectValue => + states = ObjectKey :: rest + Stream.emit(DocEvent.GroupBegin) + case FirstArrayValue => + states = ArrayValue :: rest + Stream.empty + case ArrayValue => + states = ArrayValue :: rest + arraySep + } + } + + override def doc(token: Token): Stream[Pure, DocEvent] = + token match { + case StartObject => + val res = separator() ++ startObject + states = FirstObjectKey :: states + res + case EndObject => + states = if (states.isEmpty) Nil else states.tail + endObject + case StartArray => + val res = separator() ++ startArray + states = FirstArrayValue :: states + res + case EndArray => + states = if (states.isEmpty) Nil else states.tail + endArray + case Key(value) => + separator() ++ Stream.emits( + DocEvent.GroupBegin :: DocEvent.IndentBegin :: DocEvent.Text(s""""$value":""") :: DocEvent.Line :: Nil) + case NullValue => + separator() ++ nullValue + case TrueValue => + separator() ++ trueValue + case FalseValue => + separator() ++ falseValue + case NumberValue(value) => + separator() ++ Stream.emit(DocEvent.Text(value)) + case StringValue(value) => + separator() ++ Stream.emit(DocEvent.Text(s""""$value"""")) + } + } + + } + } diff --git a/json/src/test/scala/fs2/data/json/RenderSpec.scala b/json/src/test/scala/fs2/data/json/RenderSpec.scala index ae7a721d..b7329095 100644 --- a/json/src/test/scala/fs2/data/json/RenderSpec.scala +++ b/json/src/test/scala/fs2/data/json/RenderSpec.scala @@ -59,4 +59,13 @@ object RenderSpec extends SimpleIOSuite { } + pureTest("An object should be properly pretty renderer with line width of 10") { + val input = Stream.emits("""{"field1": "test", "field2": [23, [true, null]]}""") + + println( + input.through(tokens[Fallible, Char]).through(fs2.data.text.render.pretty(width = 20, indent = 2)).compile.string) + expect(true) + + } + }