• 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

91.39
/core/src/main/scala/pureconfig/ConfigCursor.scala
1
package pureconfig
2

3
import scala.jdk.CollectionConverters._
4
import scala.util.{Failure, Success, Try}
5

6
import com.typesafe.config.ConfigValueType._
7
import com.typesafe.config._
8

9
import pureconfig.backend.PathUtil
10
import pureconfig.error._
11

12
/** A wrapper for a `ConfigValue` providing safe navigation through the config and holding positional data for better
13
  * error handling.
14
  */
15
sealed trait ConfigCursor {
16

17
  /** The optional `ConfigValue` which this cursor points to.
18
    */
19
  def valueOpt: Option[ConfigValue]
20

21
  /** The path in the config to which this cursor points as a list of keys in reverse order (deepest key first).
22
    */
23
  def pathElems: List[String]
24

25
  /** The path in the config to which this cursor points.
26
    */
27
  def path: String = PathUtil.joinPath(pathElems.reverse)
24✔
28

29
  /** The file system location of the config to which this cursor points.
30
    */
31
  def origin: Option[ConfigOrigin] = valueOpt.map(_.origin())
24✔
32

33
  /** Returns whether this cursor points to an undefined value. A cursor can point to an undefined value when a missing
34
    * config key is requested or when a `null` `ConfigValue` is provided, among other reasons.
35
    *
36
    * @return
37
    *   `true` if this cursor points to an undefined value, `false` otherwise.
38
    */
39
  def isUndefined: Boolean = valueOpt.isEmpty
7✔
40

41
  /** Returns whether this cursor points to a `null` config value. An explicit `null` value is different than a missing
42
    * value - `isUndefined` can be used to check for the latter.
43
    *
44
    * @return
45
    *   `true` if this cursor points to a `null` value, `false` otherwise.
46
    */
47
  def isNull: Boolean = valueOpt.exists(_.unwrapped == null)
4✔
48

49
  /** Casts this cursor to a `ConfigValue`.
50
    *
51
    * @return
52
    *   a `Right` with the config value pointed to by this cursor if the value is defined, `Left` with a list of
53
    *   failures otherwise.
54
    */
55
  def asConfigValue: ConfigReader.Result[ConfigValue] = {
1✔
56
    valueOpt match {
25✔
57
      case Some(value) => Right(value)
25✔
58
      case None => failed(KeyNotFound.forKeys(path, Set()))
4✔
59
    }
60
  }
61

62
  /** Casts this cursor to a string.
63
    *
64
    * @return
65
    *   a `Right` with the string value pointed to by this cursor if the cast can be done, `Left` with a list of
66
    *   failures otherwise.
67
    */
68
  def asString: ConfigReader.Result[String] =
1✔
69
    castOrFail(STRING, v => Right(v.unwrapped.asInstanceOf[String]))
21✔
70

71
  /** Casts this cursor to a boolean.
72
    *
73
    * @return
74
    *   a `Right` with the boolean value pointed to by this cursor if the cast can be done, `Left` with a list of
75
    *   failures otherwise.
76
    */
77
  def asBoolean: ConfigReader.Result[Boolean] =
1✔
78
    castOrFail(BOOLEAN, v => Right(v.unwrapped.asInstanceOf[Boolean]))
4✔
79

80
  /** Casts this cursor to a long.
81
    *
82
    * @return
83
    *   a `Right` with the long value pointed to by this cursor if the cast can be done, `Left` with a list of failures
84
    *   otherwise.
85
    */
86
  def asLong: ConfigReader.Result[Long] =
1✔
87
    castOrFail(
4✔
88
      NUMBER,
3✔
89
      _.unwrapped match {
1✔
90
        case i: java.lang.Long => Right(i)
3✔
91
        case i: java.lang.Integer => Right(i.longValue())
3✔
UNCOV
92
        case i: java.lang.Double if i.longValue().toDouble == i => Right(i.longValue())
1✔
93
        case v => Left(CannotConvert(v.toString, "Long", "Unable to convert Number to Long"))
2✔
94
      }
95
    )
96

97
  /** Casts this cursor to an int.
98
    *
99
    * @return
100
    *   a `Right` with the int value pointed to by this cursor if the cast can be done, `Left` with a list of failures
101
    *   otherwise.
102
    */
103
  def asInt: ConfigReader.Result[Int] =
1✔
104
    castOrFail(
8✔
105
      NUMBER,
7✔
106
      _.unwrapped match {
1✔
107
        case i: java.lang.Long if i.intValue().toLong == i => Right(i.intValue())
4✔
108
        case i: java.lang.Integer => Right(i)
8✔
UNCOV
109
        case i: java.lang.Double if i.intValue().toDouble == i => Right(i.intValue())
1✔
110
        case v => Left(CannotConvert(v.toString, "Int", "Unable to convert Number to Int"))
2✔
111
      }
112
    )
113

114
  /** Casts this cursor to a short.
115
    *
116
    * @return
117
    *   a `Right` with the short value pointed to by this cursor if the cast can be done, `Left` with a list of failures
118
    *   otherwise.
119
    */
120
  def asShort: ConfigReader.Result[Short] =
1✔
121
    castOrFail(
2✔
122
      NUMBER,
1✔
123
      _.unwrapped match {
1✔
124
        case i: java.lang.Long if i.shortValue().toLong == i => Right(i.shortValue())
×
125
        case i: java.lang.Integer if i.shortValue().toInt == i => Right(i.shortValue())
2✔
126
        case i: java.lang.Double if i.shortValue().toDouble == i => Right(i.shortValue())
×
127
        case v => Left(CannotConvert(v.toString, "Short", "Unable to convert Number to Short"))
2✔
128
      }
129
    )
130

131
  /** Casts this cursor to a byte.
132
    *
133
    * @return
134
    *   a `Right` with the byte value pointed to by this cursor if the cast can be done, `Left` with a list of failures
135
    *   otherwise.
136
    */
137
  def asByte: ConfigReader.Result[Byte] =
1✔
138
    castOrFail(
2✔
139
      NUMBER,
1✔
140
      _.unwrapped match {
1✔
141
        case i: java.lang.Long if i.byteValue().toLong == i => Right(i.byteValue())
×
142
        case i: java.lang.Integer if i.byteValue().toInt == i => Right(i.byteValue())
2✔
143
        case i: java.lang.Double if i.byteValue().toDouble == i => Right(i.byteValue())
×
144
        case v => Left(CannotConvert(v.toString, "Byte", "Unable to convert Number to Byte"))
×
145
      }
146
    )
147

148
  /** Casts this cursor to a double.
149
    *
150
    * @return
151
    *   a `Right` with the double value pointed to by this cursor if the cast can be done, `Left` with a list of
152
    *   failures otherwise.
153
    */
154
  def asDouble: ConfigReader.Result[Double] =
1✔
155
    castOrFail(
2✔
156
      NUMBER,
1✔
157
      _.unwrapped match {
1✔
158
        case i: java.lang.Long if i.doubleValue().toLong == i => Right(i.doubleValue())
×
159
        case i: java.lang.Integer if i.doubleValue().toInt == i => Right(i.doubleValue())
2✔
160
        case i: java.lang.Double => Right(i)
2✔
161
        case v => Left(CannotConvert(v.toString, "Double", "Unable to convert Number to Double"))
×
162
      }
163
    )
164

165
  /** Casts this cursor to a float.
166
    *
167
    * @return
168
    *   a `Right` with the float value pointed to by this cursor if the cast can be done, `Left` with a list of failures
169
    *   otherwise.
170
    */
171
  def asFloat: ConfigReader.Result[Float] =
1✔
172
    castOrFail(
2✔
173
      NUMBER,
1✔
174
      _.unwrapped match {
1✔
175
        case i: java.lang.Long if i.floatValue().toLong == i => Right(i.floatValue())
×
176
        case i: java.lang.Integer if i.floatValue().toInt == i => Right(i.floatValue())
2✔
177
        case i: java.lang.Double => Right(i.floatValue())
2✔
178
        case v => Left(CannotConvert(v.toString, "Float", "Unable to convert Number to Float"))
×
179
      }
180
    )
181

182
  /** Casts this cursor to a `ConfigListCursor`.
183
    *
184
    * @return
185
    *   a `Right` with this cursor as a list cursor if the cast can be done, `Left` with a list of failures otherwise.
186
    */
187
  def asListCursor: ConfigReader.Result[ConfigListCursor] =
1✔
188
    castOrFail(LIST, v => Right(v.asInstanceOf[ConfigList])).map(ConfigListCursor(_, pathElems))
7✔
189

190
  /** Casts this cursor to a list of cursors.
191
    *
192
    * @return
193
    *   a `Right` with the list pointed to by this cursor if the cast can be done, `Left` with a list of failures
194
    *   otherwise.
195
    */
196
  def asList: ConfigReader.Result[List[ConfigCursor]] =
1✔
197
    asListCursor.map(_.list)
4✔
198

199
  /** Casts this cursor to a `ConfigObjectCursor`.
200
    *
201
    * @return
202
    *   a `Right` with this cursor as an object cursor if it points to an object, `Left` with a list of failures
203
    *   otherwise.
204
    */
205
  def asObjectCursor: ConfigReader.Result[ConfigObjectCursor] =
1✔
206
    castOrFail(OBJECT, v => Right(v.asInstanceOf[ConfigObject])).map(ConfigObjectCursor(_, pathElems))
8✔
207

208
  /** Casts this cursor to a map from config keys to cursors.
209
    *
210
    * @return
211
    *   a `Right` with the map pointed to by this cursor if the cast can be done, `Left` with a list of failures
212
    *   otherwise.
213
    */
214
  def asMap: ConfigReader.Result[Map[String, ConfigCursor]] =
1✔
215
    asObjectCursor.map(_.map)
2✔
216

217
  def fluent: FluentConfigCursor =
1✔
218
    FluentConfigCursor(asConfigValue.map(_ => this))
7✔
219

220
  /** Returns a failed `ConfigReader` result resulting from scoping a `FailureReason` into the context of this cursor.
221
    *
222
    * This operation is the easiest way to return a failure from a `ConfigReader`.
223
    *
224
    * @param reason
225
    *   the reason of the failure
226
    * @tparam A
227
    *   the returning type of the `ConfigReader`
228
    * @return
229
    *   a failed `ConfigReader` result built by scoping `reason` into the context of this cursor.
230
    */
231
  def failed[A](reason: FailureReason): ConfigReader.Result[A] =
1✔
232
    Left(ConfigReaderFailures(failureFor(reason)))
8✔
233

234
  /** Returns a `ConfigReaderFailure` resulting from scoping a `FailureReason` into the context of this cursor.
235
    *
236
    * This operation is useful for constructing `ConfigReaderFailures` when there are multiple `FailureReason`s.
237
    *
238
    * @param reason
239
    *   the reason of the failure
240
    * @return
241
    *   a `ConfigReaderFailure` built by scoping `reason` into the context of this cursor.
242
    */
243
  def failureFor(reason: FailureReason): ConfigReaderFailure =
1✔
244
    ConvertFailure(reason, this)
24✔
245

246
  /** Returns a failed `ConfigReader` result resulting from scoping a `Either[FailureReason, A]` into the context of
247
    * this cursor.
248
    *
249
    * This operation is needed when control of the reading process is passed to a place without a `ConfigCursor`
250
    * instance providing the nexessary context (for example, when `ConfigReader.fromString` is used. In those scenarios,
251
    * the call should be wrapped in this method in order to turn `FailureReason` instances into `ConfigReaderFailures`.
252
    *
253
    * @param result
254
    *   the result of a config reading operation
255
    * @tparam A
256
    *   the returning type of the `ConfigReader`
257
    * @return
258
    *   a `ConfigReader` result built by scoping `reason` into the context of this cursor.
259
    */
260
  def scopeFailure[A](result: Either[FailureReason, A]): ConfigReader.Result[A] =
1✔
261
    result.left.map { reason => ConfigReaderFailures(failureFor(reason)) }
21✔
262

263
  private[this] def castOrFail[A](
1✔
264
      expectedType: ConfigValueType,
265
      cast: ConfigValue => Either[FailureReason, A]
266
  ): ConfigReader.Result[A] = {
267

268
    asConfigValue.flatMap { value =>
23✔
269
      scopeFailure(ConfigCursor.transform(value, expectedType).flatMap(cast))
22✔
270
    }
271
  }
272
}
273

274
object ConfigCursor {
275

276
  /** Builds a `ConfigCursor` for a `ConfigValue` at a path.
277
    *
278
    * @param value
279
    *   the `ConfigValue` to which the cursor should point
280
    * @param pathElems
281
    *   the path of `value` in the config as a list of keys
282
    * @return
283
    *   a `ConfigCursor` for `value` at the path given by `pathElems`.
284
    */
285
  def apply(value: ConfigValue, pathElems: List[String]): ConfigCursor = SimpleConfigCursor(Option(value), pathElems)
23✔
286

287
  /** Builds a `ConfigCursor` for an optional `ConfigValue` at a path.
288
    *
289
    * @param value
290
    *   the optional `ConfigValue` to which the cursor should point
291
    * @param pathElems
292
    *   the path of `value` in the config as a list of keys
293
    * @return
294
    *   a `ConfigCursor` for `value` at the path given by `pathElems`.
295
    */
296
  def apply(value: Option[ConfigValue], pathElems: List[String]): ConfigCursor = SimpleConfigCursor(value, pathElems)
3✔
297

298
  /** Handle automatic type conversions of `ConfigValue`s the way the
299
    * [[https://github.com/lightbend/config/blob/master/HOCON.md#automatic-type-conversions HOCON specification]]
300
    * describes. This code mimics the behavior of the package-private `com.typesafe.config.impl.DefaultTransformer`
301
    * class in Typesafe Config.
302
    */
303
  private[pureconfig] def transform(
1✔
304
      configValue: ConfigValue,
305
      requested: ConfigValueType
306
  ): Either[WrongType, ConfigValue] = {
307
    (configValue.valueType(), requested) match {
1✔
308
      case (valueType, requestedType) if valueType == requestedType =>
22✔
309
        Right(configValue)
22✔
310

311
      case (ConfigValueType.STRING, requestedType) =>
1✔
312
        val s = configValue.unwrapped.asInstanceOf[String]
7✔
313

314
        requestedType match {
315
          case ConfigValueType.NUMBER =>
6✔
316
            lazy val tryLong = Try(s.toLong).map(ConfigValueFactory.fromAnyRef)
1✔
317
            lazy val tryDouble = Try(s.toDouble).map(ConfigValueFactory.fromAnyRef)
1✔
318
            tryLong.orElse(tryDouble) match {
6✔
319
              case Success(value) => Right(value)
4✔
320
              case Failure(_) => Left(WrongType(configValue.valueType, Set(NUMBER)))
4✔
321
            }
322

323
          case ConfigValueType.NULL if s == "null" =>
×
324
            Right(ConfigValueFactory.fromAnyRef(null))
×
325

326
          case ConfigValueType.BOOLEAN if s == "true" || s == "yes" || s == "on" =>
2✔
327
            Right(ConfigValueFactory.fromAnyRef(true))
2✔
328

329
          case ConfigValueType.BOOLEAN if s == "false" || s == "no" || s == "off" =>
2✔
330
            Right(ConfigValueFactory.fromAnyRef(false))
2✔
331

332
          case other =>
1✔
333
            Left(WrongType(configValue.valueType(), Set(other)))
5✔
334
        }
335

336
      case (ConfigValueType.NUMBER | ConfigValueType.BOOLEAN, ConfigValueType.STRING) =>
1✔
337
        Right(ConfigValueFactory.fromAnyRef(configValue.unwrapped.toString))
4✔
338

339
      case (ConfigValueType.OBJECT, ConfigValueType.LIST) =>
1✔
340
        val obj = configValue.asInstanceOf[ConfigObject].asScala.iterator
2✔
341
        val ll = obj.flatMap { case (str, v) => Try(str.toInt).toOption.map(_ -> v) }.toList
2✔
342

343
        ll match {
344
          case l if l.nonEmpty => Right(ConfigValueFactory.fromIterable(l.sortBy(_._1).map(_._2).asJava))
2✔
345
          case _ => Left(WrongType(ConfigValueType.OBJECT, Set(ConfigValueType.LIST)))
2✔
346
        }
347

348
      case (valueType, requestedType) =>
1✔
349
        Left(WrongType(valueType, Set(requestedType)))
5✔
350
    }
351
  }
352
}
353

354
/** A simple `ConfigCursor` providing no extra operations.
355
  */
356
case class SimpleConfigCursor(valueOpt: Option[ConfigValue], pathElems: List[String]) extends ConfigCursor
357

358
/** A `ConfigCursor` pointing to a config list.
359
  */
360
case class ConfigListCursor(listValue: ConfigList, pathElems: List[String], offset: Int = 0) extends ConfigCursor {
1✔
361

362
  @inline private[this] def validIndex(idx: Int) = idx >= 0 && idx < size
3✔
363
  @inline private[this] def indexKey(idx: Int) = (offset + idx).toString
7✔
364

365
  def valueOpt: Option[ConfigList] = Some(listValue)
3✔
366

367
  override def asConfigValue: ConfigReader.Result[ConfigList] = Right(listValue)
×
368

369
  /** Returns whether the config list pointed to by this cursor is empty.
370
    */
371
  def isEmpty: Boolean = listValue.isEmpty
2✔
372

373
  /** Returns the size of the config list pointed to by this cursor.
374
    */
375
  def size: Int = listValue.size
3✔
376

377
  /** Returns a cursor to the config at a given index.
378
    *
379
    * @param idx
380
    *   the index of the config for which a cursor should be returned
381
    * @return
382
    *   a `Right` with a cursor to the config at `idx` if such a config exists, a `Left` with a list of failures
383
    *   otherwise.
384
    */
385
  def atIndex(idx: Int): ConfigReader.Result[ConfigCursor] = {
1✔
386
    atIndexOrUndefined(idx) match {
2✔
387
      case idxCur if idxCur.isUndefined => failed(KeyNotFound.forKeys(indexKey(idx), Set()))
2✔
388
      case idxCur => Right(idxCur)
2✔
389
    }
390
  }
391

392
  /** Returns a cursor to the config at a given index. An out of range index will return a cursor to an undefined value.
393
    *
394
    * @param idx
395
    *   the index of the config for which a cursor should be returned
396
    * @return
397
    *   a cursor to the config at `idx` if such a config exists, a cursor to an undefined value otherwise.
398
    */
399
  def atIndexOrUndefined(idx: Int): ConfigCursor =
1✔
400
    ConfigCursor(if (validIndex(idx)) Some(listValue.get(idx)) else None, indexKey(idx) :: pathElems)
2✔
401

402
  /** Returns a cursor to the tail of the config list pointed to by this cursor if non-empty.
403
    *
404
    * @return
405
    *   a `Some` with the tail of the config list if the list is not empty, `None` otherwise.
406
    */
407
  def tailOption: Option[ConfigListCursor] = {
1✔
408
    if (listValue.isEmpty) None
2✔
409
    else {
3✔
410
      val newValue = ConfigValueFactory
411
        .fromAnyRef(listValue.asScala.drop(1).asJava)
3✔
412
        .withOrigin(listValue.origin)
3✔
413
        .asInstanceOf[ConfigList]
2✔
414

415
      Some(ConfigListCursor(newValue, pathElems, offset = offset + 1))
2✔
416
    }
417
  }
418

419
  /** Returns a list of cursors to the elements of the config list pointed to by this cursor.
420
    *
421
    * @return
422
    *   a list of cursors to the elements of the config list pointed to by this cursor.
423
    */
424
  def list: List[ConfigCursor] =
1✔
425
    listValue.asScala.toList.drop(offset).zipWithIndex.map { case (cv, idx) =>
7✔
426
      ConfigCursor(cv, indexKey(idx) :: pathElems)
7✔
427
    }
428

429
  // Avoid resetting the offset when using ConfigCursor's implementation.
430
  override def asListCursor: ConfigReader.Result[ConfigListCursor] = Right(this)
3✔
431
}
432

433
/** A `ConfigCursor` pointing to a config object.
434
  */
435
case class ConfigObjectCursor(objValue: ConfigObject, pathElems: List[String]) extends ConfigCursor {
436

437
  def valueOpt: Option[ConfigObject] = Some(objValue)
8✔
438

439
  override def asConfigValue: ConfigReader.Result[ConfigObject] = Right(objValue)
×
440

441
  /** Returns whether the config object pointed to by this cursor is empty.
442
    */
443
  def isEmpty: Boolean = objValue.isEmpty
2✔
444

445
  /** Returns the size of the config object pointed to by this cursor.
446
    */
447
  def size: Int = objValue.size
2✔
448

449
  /** Returns the list of keys of the config object pointed to by this cursor.
450
    */
451
  def keys: Iterable[String] =
1✔
452
    objValue.keySet.asScala
8✔
453

454
  /** Returns a cursor to the config at a given key.
455
    *
456
    * @param key
457
    *   the key of the config for which a cursor should be returned
458
    * @return
459
    *   a `Right` with a cursor to the config at `key` if such a config exists, a `Left` with a list of failures
460
    *   otherwise.
461
    */
462
  def atKey(key: String): ConfigReader.Result[ConfigCursor] = {
1✔
463
    atKeyOrUndefined(key) match {
7✔
464
      case keyCur if keyCur.isUndefined => failed(KeyNotFound.forKeys(key, keys))
8✔
465
      case keyCur => Right(keyCur)
7✔
466
    }
467
  }
468

469
  /** Returns a cursor to the config at a given key. A missing key will return a cursor to an undefined value.
470
    *
471
    * @param key
472
    *   the key of the config for which a cursor should be returned
473
    * @return
474
    *   a cursor to the config at `key` if such a config exists, a cursor to an undefined value otherwise.
475
    */
476
  def atKeyOrUndefined(key: String): ConfigCursor =
1✔
477
    ConfigCursor(objValue.get(key), key :: pathElems)
7✔
478

479
  /** Returns a cursor to the object pointed to by this cursor without a given key.
480
    *
481
    * @param key
482
    *   the key to remove on the config object
483
    * @return
484
    *   a cursor to the object pointed to by this cursor without `key`.
485
    */
486
  def withoutKey(key: String): ConfigObjectCursor =
1✔
487
    ConfigObjectCursor(objValue.withoutKey(key), pathElems)
4✔
488

489
  /** Returns a map of cursors to the elements of the config object pointed to by this cursor.
490
    */
491
  def map: Map[String, ConfigCursor] =
1✔
492
    objValue.asScala.toMap.map { case (key, cv) => key -> ConfigCursor(cv, key :: pathElems) }
7✔
493

494
  // Avoid unnecessary cast.
495
  override def asObjectCursor: ConfigReader.Result[ConfigObjectCursor] = Right(this)
6✔
496
}
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