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
Showing
4 changed files
with
466 additions
and
142 deletions.
There are no files selected for viewing
174 changes: 174 additions & 0 deletions
174
modules/yaml/src/main/scala/pureconfig/module/yaml/YamlConfigSource.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,174 @@ | ||
package pureconfig.module.yaml | ||
|
||
import java.io._ | ||
import java.net.{ URI, URL } | ||
import java.nio.file.{ Files, Path, Paths } | ||
import java.util.Base64 | ||
|
||
import scala.collection.JavaConverters._ | ||
import scala.util.Try | ||
import scala.util.control.NonFatal | ||
|
||
import com.typesafe.config.{ ConfigValue, ConfigValueFactory } | ||
import org.yaml.snakeyaml.Yaml | ||
import org.yaml.snakeyaml.constructor.SafeConstructor | ||
import org.yaml.snakeyaml.error.{ Mark, MarkedYAMLException, YAMLException } | ||
import pureconfig.ConfigReader.Result | ||
import pureconfig.error._ | ||
import pureconfig.module.yaml.error.{ NonStringKeyFound, UnsupportedYamlType } | ||
import pureconfig.{ ConfigObjectSource, ConfigSource } | ||
|
||
/** | ||
* A `ConfigSource` that reads configs from YAML documents in a stream, file or string. | ||
* | ||
* @param getReader the thunk to generate a `Reader` instance from which the YAML document will be | ||
* read. This parameter won't be memoized so it can be used with dynamic sources | ||
* (e.g. URLs) | ||
* @param uri the optional URI of the source. Used only to provide better error messages. | ||
* @param onIOFailure an optional function used to provide a custom failure when IO errors happen | ||
*/ | ||
final class YamlConfigSource private ( | ||
getReader: () => Reader, | ||
uri: Option[URI] = None, | ||
onIOFailure: Option[Option[Throwable] => CannotRead] = None) extends ConfigSource { | ||
|
||
// instances of `Yaml` are not thread safe | ||
private[this] def loader = new Yaml(new SafeConstructor()) | ||
|
||
def value(): Result[ConfigValue] = { | ||
usingReader { reader => | ||
yamlObjToConfigValue(loader.load[AnyRef](reader)) | ||
} | ||
} | ||
|
||
/** | ||
* Converts this YAML source to a config object source to allow merging with other sources. This | ||
* operation is not reversible. The new source will load with an error if this document does not | ||
* contain an object. | ||
* | ||
* @return a config object source that produces YAML object documents read by this source | ||
*/ | ||
def asObjectSource: ConfigObjectSource = | ||
ConfigObjectSource(fluentCursor().asObjectCursor.right.map(_.value.toConfig)) | ||
|
||
/** | ||
* Returns a new source that produces a multi-document YAML read by this source as a config list. | ||
* | ||
* @return a new source that produces a multi-document YAML read by this source as a config list. | ||
*/ | ||
def multiDoc: ConfigSource = new ConfigSource { | ||
def value(): Result[ConfigValue] = { | ||
usingReader { reader => | ||
loader.loadAll(reader).asScala | ||
.map(yamlObjToConfigValue) | ||
.foldRight(Right(Nil): Result[List[ConfigValue]])(Result.zipWith(_, _)(_ :: _)) | ||
.right.map { cvs => ConfigValueFactory.fromIterable(cvs.asJava) } | ||
} | ||
} | ||
} | ||
|
||
// Converts an object created by SnakeYAML to a Typesafe `ConfigValue`. | ||
// (https://bitbucket.org/asomov/snakeyaml/wiki/Documentation#markdown-header-loading-yaml) | ||
private[this] def yamlObjToConfigValue(obj: AnyRef): Result[ConfigValue] = { | ||
|
||
def aux(obj: AnyRef): Result[AnyRef] = obj match { | ||
case m: java.util.Map[AnyRef @unchecked, AnyRef @unchecked] => | ||
val entries: Iterable[Result[(String, AnyRef)]] = m.asScala.map { | ||
case (k: String, v) => aux(v).right.map { v: AnyRef => k -> v } | ||
case (k, _) => Left(ConfigReaderFailures(NonStringKeyFound(k.toString, k.getClass.getSimpleName))) | ||
} | ||
Result.sequence(entries).right.map(_.toMap.asJava) | ||
|
||
case xs: java.util.List[AnyRef @unchecked] => | ||
Result.sequence(xs.asScala.map(aux)).right.map(_.toList.asJava) | ||
|
||
case s: java.util.Set[AnyRef @unchecked] => | ||
Result.sequence(s.asScala.map(aux)).right.map(_.toSet.asJava) | ||
|
||
case _: java.lang.Integer | _: java.lang.Long | _: java.lang.Double | _: java.lang.String | _: java.lang.Boolean => | ||
Right(obj) // these types are supported directly by `ConfigValueFactory.fromAnyRef` | ||
|
||
case _: java.util.Date | _: java.sql.Date | _: java.sql.Timestamp | _: java.math.BigInteger => | ||
Right(obj.toString) | ||
|
||
case ba: Array[Byte] => | ||
Right(Base64.getEncoder.encodeToString(ba)) | ||
|
||
case null => | ||
Right(null) | ||
|
||
case _ => // this shouldn't happen | ||
Left(ConfigReaderFailures(UnsupportedYamlType(obj.toString, obj.getClass.getSimpleName))) | ||
} | ||
|
||
aux(obj).right.map(ConfigValueFactory.fromAnyRef) | ||
} | ||
|
||
// Opens and processes a YAML file, converting all exceptions into the most appropriate PureConfig errors. | ||
private[this] def usingReader[A](f: Reader => Result[A]): Result[A] = { | ||
try { | ||
val reader = getReader() | ||
try f(reader) | ||
finally Try(reader.close()) | ||
} catch { | ||
case e: IOException if onIOFailure.nonEmpty => | ||
Result.fail(onIOFailure.get(Some(e))) | ||
case e: MarkedYAMLException => | ||
Result.fail(CannotParse(e.getProblem, uri.map { uri => toConfigValueLocation(uri.toURL, e.getProblemMark) })) | ||
case e: YAMLException => | ||
Result.fail(CannotParse(e.getMessage, None)) | ||
case NonFatal(e) => | ||
Result.fail(ThrowableFailure(e, None)) | ||
} | ||
} | ||
|
||
// Converts a SnakeYAML `Mark` to a `ConfigValueLocation`, provided the file path. | ||
private[this] def toConfigValueLocation(path: URL, mark: Mark): ConfigValueLocation = { | ||
ConfigValueLocation(path, mark.getLine + 1) | ||
} | ||
} | ||
|
||
object YamlConfigSource { | ||
|
||
/** | ||
* Returns a YAML source that provides configs read from a file. | ||
* | ||
* @param path the path to the file as a string | ||
* @return a YAML source that provides configs read from a file. | ||
*/ | ||
def file(path: String) = new YamlConfigSource( | ||
() => new FileReader(path), | ||
uri = Some(new File(path).toURI), | ||
onIOFailure = Some(CannotReadFile(Paths.get(path), _))) | ||
|
||
/** | ||
* Returns a YAML source that provides configs read from a file. | ||
* | ||
* @param path the path to the file | ||
* @return a YAML source that provides configs read from a file. | ||
*/ | ||
def file(path: Path) = new YamlConfigSource( | ||
() => Files.newBufferedReader(path), | ||
uri = Some(path.toUri), | ||
onIOFailure = Some(CannotReadFile(path, _))) | ||
|
||
/** | ||
* Returns a YAML source that provides configs read from a file. | ||
* | ||
* @param file the file | ||
* @return a YAML source that provides configs read from a file. | ||
*/ | ||
def file(file: File) = new YamlConfigSource( | ||
() => new FileReader(file), | ||
uri = Some(file.toURI), | ||
onIOFailure = Some(CannotReadFile(file.toPath, _))) | ||
|
||
/** | ||
* Returns a YAML source that provides a config parsed from a string. | ||
* | ||
* @param confStr the YAML content | ||
* @return a YAML source that provides a config parsed from a string. | ||
*/ | ||
def string(confStr: String) = new YamlConfigSource( | ||
() => new StringReader(confStr)) | ||
} |
Oops, something went wrong.