• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pureconfig / pureconfig / 16844894943

09 Aug 2025 03:28AM UTC coverage: 94.686% (-0.04%) from 94.726%
16844894943

Pull #1838

web-flow
Merge 12f0062cf into 38f73c635
Pull Request #1838: Rid of deprecated URL constructor

1 of 1 new or added line in 1 file covered. (100.0%)

95 existing lines in 26 files now uncovered.

2744 of 2898 relevant lines covered (94.69%)

2.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.71
/modules/yaml/src/main/scala/pureconfig/module/yaml/YamlConfigSource.scala
1
package pureconfig.module.yaml
2

3
import java.io._
4
import java.net.{URI, URL}
5
import java.nio.file.{Files, Path, Paths}
6
import java.util.Base64
7

8
import scala.jdk.CollectionConverters._
9
import scala.util.Try
10
import scala.util.chaining.scalaUtilChainingOps
11
import scala.util.control.NonFatal
12

13
import com.typesafe.config.{ConfigOrigin, ConfigOriginFactory, ConfigValue, ConfigValueFactory}
14
import org.yaml.snakeyaml.constructor.SafeConstructor
15
import org.yaml.snakeyaml.env.EnvScalarConstructor
16
import org.yaml.snakeyaml.error.{Mark, MarkedYAMLException, YAMLException}
17
import org.yaml.snakeyaml.nodes.Tag
18
import org.yaml.snakeyaml.{LoaderOptions, Yaml}
19

20
import pureconfig.ConfigReader.Result
21
import pureconfig.error._
22
import pureconfig.module.yaml.error.{NonStringKeyFound, UnsupportedYamlType}
23
import pureconfig.{ConfigObjectSource, ConfigSource}
24

25
/** A `ConfigSource` that reads configs from YAML documents in a stream, file or string.
26
  *
27
  * @param getReader
28
  *   the thunk to generate a `Reader` instance from which the YAML document will be read. This parameter won't be
29
  *   memoized so it can be used with dynamic sources (e.g. URLs)
30
  * @param uri
31
  *   the optional URI of the source. Used only to provide better error messages.
32
  * @param onIOFailure
33
  *   an optional function used to provide a custom failure when IO errors happen
34
  */
35
final class YamlConfigSource private (
36
    getReader: () => Reader,
37
    uri: Option[URI] = None,
1✔
38
    onIOFailure: Option[Option[Throwable] => CannotRead] = None,
1✔
39
    enableEnvironmentOverrides: Boolean = false
1✔
40
) extends ConfigSource {
41

42
  // instances of `Yaml` are not thread safe
43
  private[this] def loader = {
1✔
44
    if (enableEnvironmentOverrides) {
2✔
45
      new Yaml(new CustomEnvironmentOverrideConstructor()).tap {
2✔
46
        // Implicit resolver to use $ instead of !ENV tag
47
        _.addImplicitResolver(EnvScalarConstructor.ENV_TAG, EnvScalarConstructor.ENV_FORMAT, "$")
2✔
48
      }
49
    } else {
1✔
50
      new Yaml(new CustomConstructor())
2✔
51
    }
52
  }
53

54
  def value(): Result[ConfigValue] = {
1✔
55
    usingReader { reader =>
2✔
56
      yamlObjToConfigValue(loader.load[AnyRef](reader))
2✔
57
    }
58
  }
59

60
  def withEnvironmentOverrides(): YamlConfigSource = {
1✔
61
    new YamlConfigSource(getReader, uri, onIOFailure, true)
2✔
62
  }
63

64
  /** Converts this YAML source to a config object source to allow merging with other sources. This operation is not
65
    * reversible. The new source will load with an error if this document does not contain an object.
66
    *
67
    * @return
68
    *   a config object source that produces YAML object documents read by this source
69
    */
70
  def asObjectSource: ConfigObjectSource =
1✔
71
    ConfigObjectSource(fluentCursor().asObjectCursor.map(_.objValue.toConfig))
2✔
72

73
  /** Returns a new source that produces a multi-document YAML read by this source as a config list.
74
    *
75
    * @return
76
    *   a new source that produces a multi-document YAML read by this source as a config list.
77
    */
78
  def multiDoc: ConfigSource =
1✔
79
    new ConfigSource {
1✔
80
      def value(): Result[ConfigValue] = {
1✔
81
        usingReader { reader =>
2✔
82
          loader
1✔
83
            .loadAll(reader)
2✔
84
            .asScala
1✔
85
            .map(yamlObjToConfigValue)
2✔
86
            .foldRight(Right(Nil): Result[List[ConfigValue]])(Result.zipWith(_, _)(_ :: _))
2✔
87
            .map { cvs => ConfigValueFactory.fromIterable(cvs.asJava) }
2✔
88
        }
89
      }
90
    }
91

92
  // YAML has special support for timestamps and the built-in `SafeConstructor` parses values into Java `Date`
93
  // instances. However, date readers are expecting strings and the original format may matter to them. This class
94
  // specifies the string parser as the one to use for dates.
95
  private[this] class CustomConstructor extends SafeConstructor(new LoaderOptions()) {
1✔
96
    yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlStr())
2✔
97
  }
98

99
  private[this] class CustomEnvironmentOverrideConstructor extends EnvScalarConstructor {
100
    yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlStr())
2✔
101
  }
102

103
  // Converts an object created by SnakeYAML to a Typesafe `ConfigValue`.
104
  // (https://bitbucket.org/asomov/snakeyaml/wiki/Documentation#markdown-header-loading-yaml)
105
  private[this] def yamlObjToConfigValue(obj: AnyRef): Result[ConfigValue] = {
1✔
106

107
    def aux(obj: AnyRef): Result[AnyRef] =
1✔
108
      obj match {
109
        case m: java.util.Map[AnyRef @unchecked, AnyRef @unchecked] =>
2✔
110
          val entries: Iterable[Result[(String, AnyRef)]] = m.asScala.map {
2✔
111
            case (k: String, v) => aux(v).map { (v: AnyRef) => k -> v }
2✔
112
            case (k, _) => Left(ConfigReaderFailures(NonStringKeyFound(k.toString, k.getClass.getSimpleName)))
2✔
113
          }
114
          Result.sequence(entries).map(_.toMap.asJava)
2✔
115

116
        case xs: java.util.List[AnyRef @unchecked] =>
1✔
117
          Result.sequence(xs.asScala.map(aux)).map(_.toList.asJava)
2✔
118

119
        case s: java.util.Set[AnyRef @unchecked] =>
1✔
120
          Result.sequence(s.asScala.map(aux)).map(_.toSet.asJava)
2✔
121

122
        case _: java.lang.Integer | _: java.lang.Long | _: java.lang.Double | _: java.lang.String |
123
            _: java.lang.Boolean =>
1✔
124
          Right(obj) // these types are supported directly by `ConfigValueFactory.fromAnyRef`
2✔
125

126
        case _: java.math.BigInteger =>
1✔
127
          Right(obj.toString)
2✔
128

129
        case ba: Array[Byte] =>
1✔
130
          Right(Base64.getEncoder.encodeToString(ba))
2✔
131

132
        case null =>
1✔
133
          Right(null)
2✔
134

UNCOV
135
        case _ => // this shouldn't happen
136
          Left(ConfigReaderFailures(UnsupportedYamlType(obj.toString, obj.getClass.getSimpleName)))
×
137
      }
138

139
    aux(obj).map(ConfigValueFactory.fromAnyRef)
2✔
140
  }
141

142
  // Opens and processes a YAML file, converting all exceptions into the most appropriate PureConfig errors.
143
  private[this] def usingReader[A](f: Reader => Result[A]): Result[A] = {
1✔
144
    try {
2✔
145
      val reader = getReader()
2✔
146
      try f(reader)
2✔
147
      finally Try(reader.close())
2✔
148
    } catch {
149
      case e: IOException if onIOFailure.nonEmpty =>
2✔
150
        Result.fail(onIOFailure.get(Some(e)))
2✔
151
      case e: MarkedYAMLException =>
1✔
152
        Result.fail(CannotParse(e.getProblem, uri.map { uri => toConfigOrigin(uri.toURL, e.getProblemMark) }))
2✔
UNCOV
153
      case e: YAMLException =>
154
        Result.fail(CannotParse(e.getMessage, None))
×
UNCOV
155
      case NonFatal(e) =>
156
        Result.fail(ThrowableFailure(e, None))
×
157
    }
158
  }
159

160
  // Converts a SnakeYAML `Mark` to a `ConfigOrigin`, provided the file path.
161
  private[this] def toConfigOrigin(path: URL, mark: Mark): ConfigOrigin = {
1✔
162
    ConfigOriginFactory.newURL(path).withLineNumber(mark.getLine + 1)
2✔
163
  }
164
}
165

166
object YamlConfigSource {
167

168
  /** Returns a YAML source that provides configs read from a file.
169
    *
170
    * @param path
171
    *   the path to the file as a string
172
    * @return
173
    *   a YAML source that provides configs read from a file.
174
    */
UNCOV
175
  def file(path: String) =
176
    new YamlConfigSource(
×
177
      () => new FileReader(path),
×
178
      uri = Some(new File(path).toURI),
×
179
      onIOFailure = Some(CannotReadFile(Paths.get(path), _))
×
180
    )
181

182
  /** Returns a YAML source that provides configs read from a file.
183
    *
184
    * @param path
185
    *   the path to the file
186
    * @return
187
    *   a YAML source that provides configs read from a file.
188
    */
189
  def file(path: Path) =
1✔
190
    new YamlConfigSource(
2✔
191
      () => Files.newBufferedReader(path),
2✔
192
      uri = Some(path.toUri),
2✔
193
      onIOFailure = Some(CannotReadFile(path, _))
1✔
194
    )
195

196
  /** Returns a YAML source that provides configs read from a file.
197
    *
198
    * @param file
199
    *   the file
200
    * @return
201
    *   a YAML source that provides configs read from a file.
202
    */
UNCOV
203
  def file(file: File) =
204
    new YamlConfigSource(
×
205
      () => new FileReader(file),
×
206
      uri = Some(file.toURI),
×
207
      onIOFailure = Some(CannotReadFile(file.toPath, _))
×
208
    )
209

210
  /** Returns a YAML source that provides a config parsed from a string.
211
    *
212
    * @param confStr
213
    *   the YAML content
214
    * @return
215
    *   a YAML source that provides a config parsed from a string.
216
    */
217
  def string(confStr: String) = new YamlConfigSource(() => new StringReader(confStr))
2✔
218
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc