-
Notifications
You must be signed in to change notification settings - Fork 51
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
1 parent
36f5a9b
commit a8672fb
Showing
20 changed files
with
1,025 additions
and
0 deletions.
There are no files selected for viewing
179 changes: 179 additions & 0 deletions
179
circe-yaml-v12/src/main/scala/io/circe/yaml/v12/Parser.scala
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,179 @@ | ||
package io.circe.yaml.v12 | ||
|
||
import cats.syntax.either._ | ||
import io.circe._ | ||
import java.io.{ Reader, StringReader } | ||
import java.util.Optional | ||
import org.snakeyaml.engine.v2.api.LoadSettings | ||
import org.snakeyaml.engine.v2.composer.Composer | ||
import org.snakeyaml.engine.v2.constructor.StandardConstructor | ||
import org.snakeyaml.engine.v2.nodes._ | ||
import org.snakeyaml.engine.v2.scanner.StreamReader | ||
import scala.collection.JavaConverters._ | ||
|
||
trait Parser { | ||
|
||
/** | ||
* Parse YAML from the given [[Reader]], returning either [[ParsingFailure]] or [[Json]] | ||
* | ||
* @param yaml | ||
* @return | ||
*/ | ||
def parse(yaml: Reader): Either[ParsingFailure, Json] | ||
def parse(yaml: String): Either[ParsingFailure, Json] | ||
def parseDocuments(yaml: Reader): Stream[Either[ParsingFailure, Json]] | ||
def parseDocuments(yaml: String): Stream[Either[ParsingFailure, Json]] | ||
} | ||
|
||
class ParserImpl(settings: LoadSettings) extends Parser { | ||
|
||
/** | ||
* Parse YAML from the given [[Reader]], returning either [[ParsingFailure]] or [[Json]] | ||
* @param yaml | ||
* @return | ||
*/ | ||
def parse(yaml: Reader): Either[ParsingFailure, Json] = for { | ||
parsed <- parseSingle(yaml) | ||
json <- yamlToJson(parsed) | ||
} yield json | ||
|
||
def parse(yaml: String): Either[ParsingFailure, Json] = | ||
parse(new StringReader(yaml)) | ||
|
||
def parseDocuments(yaml: Reader): Stream[Either[ParsingFailure, Json]] = parseStream(yaml).map(yamlToJson) | ||
def parseDocuments(yaml: String): Stream[Either[ParsingFailure, Json]] = parseDocuments(new StringReader(yaml)) | ||
|
||
private[this] def asScala[T](ot: Optional[T]): Option[T] = | ||
if (ot.isPresent) Some(ot.get()) else None | ||
|
||
private[this] def createComposer(reader: Reader) = | ||
new Composer(settings, new org.snakeyaml.engine.v2.parser.ParserImpl(settings, new StreamReader(settings, reader))) | ||
|
||
private[this] def parseSingle(reader: Reader): Either[ParsingFailure, Node] = | ||
Either.catchNonFatal { | ||
val composer = createComposer(reader) | ||
asScala(composer.getSingleNode) | ||
} match { | ||
case Left(err) => Left(ParsingFailure(err.getMessage, err)) | ||
case Right(None) => Left(ParsingFailure("no document found", new RuntimeException("no document found"))) | ||
case Right(Some(value)) => Right(value) | ||
} | ||
|
||
private[this] def parseStream(reader: Reader) = | ||
createComposer(reader).asScala.toStream | ||
|
||
private[this] object CustomTag { | ||
def unapply(tag: Tag): Option[String] = if (!tag.getValue.startsWith(Tag.PREFIX)) | ||
Some(tag.getValue) | ||
else | ||
None | ||
} | ||
|
||
private[this] class FlatteningConstructor(settings: LoadSettings) extends StandardConstructor(settings) { | ||
def flatten(node: MappingNode): MappingNode = { | ||
flattenMapping(node) | ||
node | ||
} | ||
|
||
def construct(node: ScalarNode): Object = super.construct(node) // to make the method public | ||
} | ||
|
||
private[this] def yamlToJson(node: Node): Either[ParsingFailure, Json] = { | ||
// Isn't thread-safe internally, may hence not be shared | ||
val flattener: FlatteningConstructor = new FlatteningConstructor(settings) | ||
|
||
def convertScalarNode(node: ScalarNode) = Either | ||
.catchNonFatal(node.getTag match { | ||
case Tag.INT if node.getValue.startsWith("0x") || node.getValue.contains("_") => | ||
Json.fromJsonNumber(flattener.construct(node) match { | ||
case int: Integer => JsonLong(int.toLong) | ||
case long: java.lang.Long => JsonLong(long) | ||
case bigint: java.math.BigInteger => | ||
JsonDecimal(bigint.toString) | ||
case other => throw new NumberFormatException(s"Unexpected number type: ${other.getClass}") | ||
}) | ||
case Tag.INT | Tag.FLOAT => | ||
JsonNumber.fromString(node.getValue).map(Json.fromJsonNumber).getOrElse { | ||
throw new NumberFormatException(s"Invalid numeric string ${node.getValue}") | ||
} | ||
case Tag.BOOL => | ||
Json.fromBoolean(flattener.construct(node) match { | ||
case b: java.lang.Boolean => b | ||
case _ => throw new IllegalArgumentException(s"Invalid boolean string ${node.getValue}") | ||
}) | ||
case Tag.NULL => Json.Null | ||
case CustomTag(other) => | ||
Json.fromJsonObject(JsonObject.singleton(other.stripPrefix("!"), Json.fromString(node.getValue))) | ||
case _ => Json.fromString(node.getValue) | ||
}) | ||
.leftMap { err => | ||
ParsingFailure(err.getMessage, err) | ||
} | ||
|
||
def convertKeyNode(node: Node) = node match { | ||
case scalar: ScalarNode => Right(scalar.getValue) | ||
case _ => Left(ParsingFailure("Only string keys can be represented in JSON", null)) | ||
} | ||
|
||
if (node == null) { | ||
Right(Json.False) | ||
} else { | ||
node match { | ||
case mapping: MappingNode => | ||
flattener | ||
.flatten(mapping) | ||
.getValue | ||
.asScala | ||
.foldLeft( | ||
Either.right[ParsingFailure, JsonObject](JsonObject.empty) | ||
) { (objEither, tup) => | ||
for { | ||
obj <- objEither | ||
key <- convertKeyNode(tup.getKeyNode) | ||
value <- yamlToJson(tup.getValueNode) | ||
} yield obj.add(key, value) | ||
} | ||
.map(Json.fromJsonObject) | ||
case sequence: SequenceNode => | ||
sequence.getValue.asScala | ||
.foldLeft(Either.right[ParsingFailure, List[Json]](List.empty[Json])) { (arrEither, node) => | ||
for { | ||
arr <- arrEither | ||
value <- yamlToJson(node) | ||
} yield value :: arr | ||
} | ||
.map(arr => Json.fromValues(arr.reverse)) | ||
case scalar: ScalarNode => convertScalarNode(scalar) | ||
} | ||
} | ||
} | ||
} | ||
|
||
object Parser { | ||
final case class Config( | ||
allowDuplicateKeys: Boolean = false, | ||
allowRecursiveKeys: Boolean = false, | ||
bufferSize: Int = 1024, | ||
label: String = "reader", | ||
maxAliasesForCollections: Int = 50, | ||
parseComments: Boolean = false, | ||
useMarks: Boolean = true | ||
) | ||
|
||
def make(config: Config = Config()): Parser = { | ||
import config._ | ||
new ParserImpl( | ||
LoadSettings.builder | ||
.setAllowDuplicateKeys(allowDuplicateKeys) | ||
.setAllowRecursiveKeys(allowRecursiveKeys) | ||
.setBufferSize(bufferSize) | ||
.setLabel(label) | ||
.setMaxAliasesForCollections(maxAliasesForCollections) | ||
.setParseComments(parseComments) | ||
.setUseMarks(useMarks) | ||
.build | ||
) | ||
} | ||
|
||
lazy val default: Parser = make() | ||
} |
172 changes: 172 additions & 0 deletions
172
circe-yaml-v12/src/main/scala/io/circe/yaml/v12/Printer.scala
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,172 @@ | ||
package io.circe.yaml.v12 | ||
|
||
import io.circe._ | ||
import io.circe.yaml.v12.Printer._ | ||
import java.io.StringWriter | ||
import org.snakeyaml.engine.v2.api.{ DumpSettings, StreamDataWriter } | ||
import org.snakeyaml.engine.v2.common | ||
import org.snakeyaml.engine.v2.emitter.Emitter | ||
import org.snakeyaml.engine.v2.nodes._ | ||
import org.snakeyaml.engine.v2.serializer.Serializer | ||
import scala.collection.JavaConverters._ | ||
|
||
trait Printer { | ||
def pretty(json: Json): String | ||
} | ||
|
||
class PrinterImpl( | ||
stringStyle: StringStyle, | ||
preserveOrder: Boolean, | ||
dropNullKeys: Boolean, | ||
mappingStyle: FlowStyle, | ||
sequenceStyle: FlowStyle, | ||
options: DumpSettings | ||
) extends Printer { | ||
|
||
import PrinterImpl._ | ||
|
||
def pretty(json: Json): String = { | ||
val writer = new StreamToStringWriter | ||
val serializer = new Serializer(options, new Emitter(options, writer)) | ||
serializer.emitStreamStart() | ||
serializer.serializeDocument(jsonToYaml(json)) | ||
serializer.emitStreamEnd() | ||
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): common.ScalarStyle = | ||
if (isBad(value)) common.ScalarStyle.DOUBLE_QUOTED else common.ScalarStyle.PLAIN | ||
|
||
private def stringScalarStyle(value: String): common.ScalarStyle = | ||
if (isBad(value)) common.ScalarStyle.DOUBLE_QUOTED | ||
else if (stringStyle == StringStyle.Plain && hasNewline(value)) common.ScalarStyle.LITERAL | ||
else StringStyle.toScalarStyle(stringStyle) | ||
|
||
private def scalarNode(tag: Tag, value: String) = new ScalarNode(tag, value, scalarStyle(value)) | ||
private def stringNode(value: String) = new ScalarNode(Tag.STR, value, stringScalarStyle(value)) | ||
private def keyNode(value: String) = new ScalarNode(Tag.STR, value, scalarStyle(value)) | ||
|
||
private def jsonToYaml(json: Json): Node = { | ||
|
||
def convertObject(obj: JsonObject) = { | ||
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) common.FlowStyle.FLOW else common.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) common.FlowStyle.FLOW else common.FlowStyle.BLOCK | ||
), | ||
obj => convertObject(obj) | ||
) | ||
} | ||
} | ||
|
||
object PrinterImpl { | ||
private def numberTag(number: JsonNumber): Tag = | ||
if (number.toString.contains(".")) Tag.FLOAT else Tag.INT | ||
|
||
private class StreamToStringWriter extends StringWriter with StreamDataWriter { | ||
override def flush(): Unit = super.flush() // to fix "conflicting members" | ||
} | ||
} | ||
|
||
object Printer { | ||
final case class Config( | ||
preserveOrder: Boolean = false, | ||
dropNullKeys: Boolean = false, | ||
indent: Int = 2, | ||
maxScalarWidth: Int = 80, | ||
splitLines: Boolean = true, | ||
indicatorIndent: Int = 0, | ||
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 | ||
) | ||
|
||
def make(config: Config = Config()): Printer = { | ||
import config._ | ||
new PrinterImpl( | ||
stringStyle, | ||
preserveOrder, | ||
dropNullKeys, | ||
mappingStyle, | ||
sequenceStyle, | ||
DumpSettings | ||
.builder() | ||
.setIndent(indent) | ||
.setWidth(maxScalarWidth) | ||
.setSplitLines(splitLines) | ||
.setIndicatorIndent(indicatorIndent) | ||
.setTagDirective(tags.asJava) | ||
.setDefaultScalarStyle(StringStyle.toScalarStyle(stringStyle)) | ||
.setExplicitStart(explicitStart) | ||
.setExplicitEnd(explicitEnd) | ||
.setBestLineBreak { | ||
lineBreak match { | ||
case LineBreak.Unix => "\n" | ||
case LineBreak.Windows => "\r\n" | ||
case LineBreak.Mac => "\r" | ||
} | ||
} | ||
.build() | ||
) | ||
} | ||
|
||
lazy val spaces2: Printer = make() | ||
lazy val spaces4: Printer = make(Config(indent = 4)) | ||
|
||
sealed trait FlowStyle | ||
object FlowStyle { | ||
case object Flow extends FlowStyle | ||
case object Block extends FlowStyle | ||
} | ||
|
||
sealed trait StringStyle | ||
object StringStyle { | ||
case object Plain extends StringStyle | ||
case object DoubleQuoted extends StringStyle | ||
case object SingleQuoted extends StringStyle | ||
case object Literal extends StringStyle | ||
case object Folded extends StringStyle | ||
|
||
def toScalarStyle(style: StringStyle): common.ScalarStyle = style match { | ||
case StringStyle.Plain => common.ScalarStyle.PLAIN | ||
case StringStyle.DoubleQuoted => common.ScalarStyle.DOUBLE_QUOTED | ||
case StringStyle.SingleQuoted => common.ScalarStyle.SINGLE_QUOTED | ||
case StringStyle.Literal => common.ScalarStyle.LITERAL | ||
case StringStyle.Folded => common.ScalarStyle.FOLDED | ||
} | ||
} | ||
|
||
sealed trait LineBreak | ||
object LineBreak { | ||
case object Unix extends LineBreak | ||
case object Windows extends LineBreak | ||
case object Mac extends LineBreak | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
circe-yaml-v12/src/main/scala/io/circe/yaml/v12/parser/package.scala
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,20 @@ | ||
package io.circe.yaml.v12 | ||
|
||
import io.circe._ | ||
import java.io.Reader | ||
|
||
package object parser { | ||
|
||
/** | ||
* Parse YAML from the given [[Reader]], returning either [[ParsingFailure]] or [[Json]] | ||
* @param yaml | ||
* @return | ||
*/ | ||
def parse(yaml: Reader): Either[ParsingFailure, Json] = Parser.default.parse(yaml) | ||
|
||
def parse(yaml: String): Either[ParsingFailure, Json] = Parser.default.parse(yaml) | ||
|
||
def parseDocuments(yaml: Reader): Stream[Either[ParsingFailure, Json]] = Parser.default.parseDocuments(yaml) | ||
def parseDocuments(yaml: String): Stream[Either[ParsingFailure, Json]] = Parser.default.parseDocuments(yaml) | ||
|
||
} |
11 changes: 11 additions & 0 deletions
11
circe-yaml-v12/src/main/scala/io/circe/yaml/v12/printer/package.scala
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,11 @@ | ||
package io.circe.yaml.v12 | ||
|
||
import io.circe.Json | ||
|
||
package object printer { | ||
|
||
/** | ||
* A default printer implementation using Snake YAML. | ||
*/ | ||
def print(tree: Json): String = Printer.spaces2.pretty(tree) | ||
} |
Oops, something went wrong.