All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pureconfig.module.yaml.package.scala Maven / Gradle / Ivy

There is a newer version: 0.17.8
Show newest version
package pureconfig.module

import java.io.IOException
import java.nio.file.{ Files, Path }
import java.util.Base64

import scala.collection.JavaConverters._
import scala.reflect.ClassTag
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._
import pureconfig.error._
import pureconfig.module.yaml.error.{ NonStringKeyFound, UnsupportedYamlType }

package object yaml {

  // 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): ConfigReader.Result[ConfigValue] = {

    def aux(obj: AnyRef): ConfigReader.Result[AnyRef] = obj match {
      case m: java.util.Map[AnyRef @unchecked, AnyRef @unchecked] =>
        val entries: Iterable[ConfigReader.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)))
        }
        ConfigReader.Result.sequence(entries).right.map(_.toMap.asJava)

      case xs: java.util.List[AnyRef @unchecked] =>
        ConfigReader.Result.sequence(xs.asScala.map(aux)).right.map(_.toList.asJava)

      case s: java.util.Set[AnyRef @unchecked] =>
        ConfigReader.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)
  }

  // Converts a SnakeYAML `Mark` to a `ConfigValueLocation`, provided the file path.
  private[this] def toConfigValueLocation(path: Path, mark: Mark): ConfigValueLocation = {
    ConfigValueLocation(path.toUri.toURL, mark.getLine + 1)
  }

  private[this] def using[A <: AutoCloseable, B](resource: => A)(f: A ⇒ B): B = {
    try f(resource)
    finally Try(resource.close())
  }

  // Opens and processes a YAML file, converting all exceptions into the most appropriate PureConfig errors.
  private[this] def handleYamlErrors[A](path: Option[Path])(block: => ConfigReader.Result[A]): ConfigReader.Result[A] = {
    try block
    catch {
      case ex: IOException if path.isDefined => Left(ConfigReaderFailures(CannotReadFile(path.get, Some(ex))))
      case ex: MarkedYAMLException => Left(ConfigReaderFailures(
        CannotParse(ex.getProblem, path.map(toConfigValueLocation(_, ex.getProblemMark)))))
      case ex: YAMLException => Left(ConfigReaderFailures(CannotParse(ex.getMessage, None)))
      case NonFatal(ex) => Left(ConfigReaderFailures(ThrowableFailure(ex, None)))
    }
  }

  /**
   * Loads a configuration of type `Config` from the given YAML file.
   *
   * @param path the path of the YAML file to read
   * @return A `Success` with the configuration if it is possible to create an instance of type
   *         `Config` from the YAML file, else a `Failure` with details on why it isn't possible
   */
  def loadYaml[Config](path: Path)(implicit reader: Derivation[ConfigReader[Config]]): ConfigReader.Result[Config] = {
    handleYamlErrors(Some(path)) {
      using(Files.newBufferedReader(path)) { ioReader =>
        // we are using `SafeConstructor` in order to avoid creating custom Java instances, leaking the PureConfig
        // abstraction over SnakeYAML
        val yamlObj = new Yaml(new SafeConstructor()).load[AnyRef](ioReader)

        yamlObjToConfigValue(yamlObj).right.flatMap { cv =>
          reader.value.from(ConfigCursor(cv, Nil))
        }
      }
    }
  }

  /**
   * Loads a configuration of type `Config` from the given string.
   *
   * @param content the string containing the YAML document
   * @return A `Success` with the configuration if it is possible to create an instance of type
   *         `Config` from `content`, else a `Failure` with details on why it isn't possible
   */
  def loadYaml[Config](content: String)(implicit reader: Derivation[ConfigReader[Config]]): ConfigReader.Result[Config] = {
    handleYamlErrors(None) {
      // we are using `SafeConstructor` in order to avoid creating custom Java instances, leaking the PureConfig
      // abstraction over SnakeYAML
      val yamlObj = new Yaml(new SafeConstructor()).load[AnyRef](content)

      yamlObjToConfigValue(yamlObj).right.flatMap { cv =>
        reader.value.from(ConfigCursor(cv, Nil))
      }
    }
  }

  /**
   * Loads a configuration of type `Config` from the given YAML file.
   *
   * @param path the path of the YAML file to read
   * @return the configuration
   */
  @throws[ConfigReaderException[_]]
  def loadYamlOrThrow[Config: ClassTag](path: Path)(implicit reader: Derivation[ConfigReader[Config]]): Config = {
    loadYaml(path) match {
      case Right(config) => config
      case Left(failures) => throw new ConfigReaderException[Config](failures)
    }
  }

  /**
   * Loads a configuration of type `Config` from the given string.
   *
   * @param content the string containing the YAML document
   * @return the configuration
   */
  @throws[ConfigReaderException[_]]
  def loadYamlOrThrow[Config: ClassTag](content: String)(implicit reader: Derivation[ConfigReader[Config]]): Config = {
    loadYaml(content) match {
      case Right(config) => config
      case Left(failures) => throw new ConfigReaderException[Config](failures)
    }
  }

  /**
   * Loads a configuration of type `Config` from the given multi-document YAML file. `Config` must have a
   * `ConfigReader` supporting reading from config lists.
   *
   * @param path the path of the YAML file to read
   * @return A `Success` with the configuration if it is possible to create an instance of type
   *         `Config` from the multi-document YAML file, else a `Failure` with details on why it
   *         isn't possible
   */
  def loadYamls[Config](path: Path)(implicit reader: Derivation[ConfigReader[Config]]): ConfigReader.Result[Config] = {
    handleYamlErrors(Some(path)) {
      using(Files.newBufferedReader(path)) { ioReader =>
        // we are using `SafeConstructor` in order to avoid creating custom Java instances, leaking the PureConfig
        // abstraction over SnakeYAML
        val yamlObjs = new Yaml(new SafeConstructor()).loadAll(ioReader)

        yamlObjs.asScala.map(yamlObjToConfigValue)
          .foldRight(Right(Nil): ConfigReader.Result[List[AnyRef]])(ConfigReader.Result.zipWith(_, _)(_ :: _))
          .right.flatMap { cvs =>
            val cl = ConfigValueFactory.fromAnyRef(cvs.asJava)
            reader.value.from(ConfigCursor(cl, Nil))
          }
      }
    }
  }

  /**
   * Loads a configuration of type `Config` from the given multi-document string. `Config` must have a
   * `ConfigReader` supporting reading from config lists.
   *
   * @param content the string containing the YAML documents
   * @return A `Success` with the configuration if it is possible to create an instance of type
   *         `Config` from the multi-document string, else a `Failure` with details on why it
   *         isn't possible
   */
  def loadYamls[Config](content: String)(implicit reader: Derivation[ConfigReader[Config]]): ConfigReader.Result[Config] = {
    handleYamlErrors(None) {
      // we are using `SafeConstructor` in order to avoid creating custom Java instances, leaking the PureConfig
      // abstraction over SnakeYAML
      val yamlObjs = new Yaml(new SafeConstructor()).loadAll(content)

      yamlObjs.asScala.map(yamlObjToConfigValue)
        .foldRight(Right(Nil): ConfigReader.Result[List[AnyRef]])(ConfigReader.Result.zipWith(_, _)(_ :: _))
        .right.flatMap { cvs =>
          val cl = ConfigValueFactory.fromAnyRef(cvs.asJava)
          reader.value.from(ConfigCursor(cl, Nil))
        }
    }
  }

  /**
   * Loads a configuration of type `Config` from the given multi-document YAML file. `Config` must have a
   * `ConfigReader` supporting reading from config lists.
   *
   * @param path the path of the YAML file to read
   * @return the configuration
   */
  @throws[ConfigReaderException[_]]
  def loadYamlsOrThrow[Config: ClassTag](path: Path)(implicit reader: Derivation[ConfigReader[Config]]): Config = {
    loadYamls(path) match {
      case Right(config) => config
      case Left(failures) => throw new ConfigReaderException[Config](failures)
    }
  }

  /**
   * Loads a configuration of type `Config` from the given multi-document string. `Config` must have a
   * `ConfigReader` supporting reading from config lists.
   *
   * @param content the string containing the YAML documents
   * @return the configuration
   */
  @throws[ConfigReaderException[_]]
  def loadYamlsOrThrow[Config: ClassTag](content: String)(implicit reader: Derivation[ConfigReader[Config]]): Config = {
    loadYamls(content) match {
      case Right(config) => config
      case Left(failures) => throw new ConfigReaderException[Config](failures)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy