-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Config.scala
592 lines (495 loc) · 23.4 KB
/
Config.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
/*
* Copyright 2022-2024 John A. De Goes and the ZIO Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package zio
import scala.annotation.tailrec
import scala.util.control.{NonFatal, NoStackTrace}
/**
* A [[zio.Config]] describes the structure of some configuration data.
*/
sealed trait Config[+A] { self =>
/**
* Returns a new config that is the composition of this config and the
* specified config.
*/
def ++[B](that: => Config[B])(implicit zippable: Zippable[A, B]): Config[zippable.Out] =
Config.Zipped[A, B, zippable.Out](self, Config.defer(that), zippable)
/**
* Returns a config whose structure is preferentially described by this
* config, but which falls back to the specified config if there is an issue
* reading from this config.
*/
def ||[A1 >: A](that: => Config[A1]): Config[A1] = Config.Fallback(self, Config.defer(that))
/**
* Adds a description to this configuration, which is intended for humans.
*/
def ??(label: => String): Config[A] = Config.defer(Config.Described(self, label))
/**
* Returns a new config whose structure is the same as this one, but which
* produces a different Scala value, constructed using the specified function.
*/
def map[B](f: A => B): Config[B] = self.mapOrFail(a => Right(f(a)))
/**
* Returns a new config whose structure is the same as this one, but which may
* produce a different Scala value, constructed using the specified fallible
* function.
*/
def mapOrFail[B](f: A => Either[Config.Error, B]): Config[B] = Config.MapOrFail(self, f)
/**
* Returns a new config whose structure is the same as this one, but which may
* produce a different Scala value, constructed using the specified function,
* which may throw exceptions that will be translated into validation errors.
*/
def mapAttempt[B](f: A => B): Config[B] =
self.mapOrFail { a =>
try Right(f(a))
catch {
case NonFatal(e) => Left(Config.Error.InvalidData(Chunk.empty, e.getMessage))
}
}
/**
* Returns a new config that has this configuration nested as a property of
* the specified name.
*/
def nested(name: => String): Config[A] =
Config.defer(Config.Nested(name, self))
/**
* Returns a new config that has this configuration nested as a property of
* the specified name.
*/
def nested(name: => String, names: String*): Config[A] =
Config.defer(Config.Nested(name, names.foldRight(self)((name, config) => Config.Nested(name, config))))
/**
* Returns an optional version of this config, which will be `None` if the
* data is missing from configuration, and `Some` otherwise.
*/
def optional: Config[Option[A]] = Config.Optional(self)
/**
* A named version of `||`.
*/
def orElse[A1 >: A](that: => Config[A1]): Config[A1] = self || that
/**
* Returns configuration which reads from this configuration, but which falls
* back to the specified configuration if reading from this configuration
* fails with an error satisfying the specified predicate.
*/
def orElseIf(condition: Config.Error => Boolean): Config.OrElse[A] =
new Config.OrElse(self, condition)
/**
* Returns a new config that describes a sequence of values, each of which has
* the structure of this config.
*/
def repeat: Config[Chunk[A]] = Config.Sequence(self)
/**
* Returns a new configuration which reads from this configuration and uses
* the resulting value to determine the configuration to read from.
*/
def switch[A1 >: A, B](f: (A1, Config[B])*): Config[B] =
Config.Switch(self, f.toMap)
/**
* Returns a new config that describes the same structure as this one, but
* which performs validation during loading.
*/
def validate(message: => String)(f: A => Boolean): Config[A] =
self.mapOrFail(a => if (!f(a)) Left(Config.Error.InvalidData(Chunk.empty, message)) else Right(a))
/**
* Returns a new config whose structure is the same as this one, but which may
* produce a different Scala value, constructed using the specified partial
* function, failing with the specified validation error if the partial
* function is not defined.
*/
def validateWith[B](message: => String)(pf: PartialFunction[A, B]): Config[B] =
self.mapOrFail(a => pf.lift(a).toRight(Config.Error.InvalidData(Chunk.empty, message)))
/**
* Returns a new config that describes the same structure as this one, but has
* the specified default value in case the information cannot be found.
*/
def withDefault[A1 >: A](default: => A1): Config[A1] =
self.orElseIf(_.isMissingDataOnly)(Config.succeed(default))
/**
* A named version of `++`.
*/
def zip[B](that: => Config[B])(implicit z: Zippable[A, B]): Config[z.Out] = self ++ that
/**
* Returns a new configuration that is the composition of this configuration
* and the specified configuration, combining their values using the function
* `f`.
*/
def zipWith[B, C](that: => Config[B])(f: (A, B) => C): Config[C] =
self.zip(that).map(f.tupled)
}
object Config {
final class Secret private (private val raw: Array[Char]) { self =>
override def equals(that: Any): Boolean =
that match {
case that: Secret =>
self.raw.length == that.raw.length &&
(0 until raw.length).foldLeft(true) { (b, i) =>
self.raw(i) == that.raw(i) && b
}
case _ => false
}
override def hashCode(): Int = value.hashCode
override def toString(): String = "Secret(<redacted>)"
object unsafe {
def wipe(implicit unsafe: Unsafe): Unit =
(0 until raw.length).foreach(i => raw(i) = 0)
}
def value: Chunk[Char] = Chunk.fromArray(raw)
}
object Secret extends (Chunk[Char] => Secret) {
def apply(chunk: Chunk[Char]): Secret = new Secret(chunk.toArray)
def apply(cs: CharSequence): Secret = Secret(cs.toString())
def apply(s: String): Secret = Secret(Chunk.fromArray(s.toCharArray))
def unapply(secret: Secret): Some[Chunk[Char]] = Some(secret.value)
}
sealed trait Primitive[+A] extends Config[A] { self =>
final def description: String =
(self: Primitive[_]) match {
case Bool => "a boolean property"
case Constant(_) => "a constant property"
case Decimal => "a decimal property"
case Duration => "a duration property"
case Fail(_) => "a static failure"
case Integer => "an integer property"
case LocalDateTime => "a local date-time property"
case LocalDate => "a local date property"
case LocalTime => "a local time property"
case OffsetDateTime => "an offset date-time property"
case SecretType => "a secret property"
case Text => "a text property"
}
final def missingError(name: String): Config.Error =
Config.Error.MissingData(Chunk.empty, s"Expected ${description} with name ${name}")
def parse(text: String): Either[Config.Error, A]
}
sealed trait Composite[+A] extends Config[A]
case object Bool extends Primitive[Boolean] {
final def parse(text: String): Either[Config.Error, Boolean] = text.toLowerCase match {
case "true" | "yes" | "on" | "1" => Right(true)
case "false" | "no" | "off" | "0" => Right(false)
case _ => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a boolean value, but found ${text}"))
}
}
final case class Constant[A](value: A) extends Primitive[A] {
final def parse(text: String): Either[Config.Error, A] = Right(value)
}
case object Decimal extends Primitive[BigDecimal] {
final def parse(text: String): Either[Config.Error, BigDecimal] = try Right(BigDecimal(text))
catch {
case NonFatal(e) => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a decimal value, but found ${text}"))
}
}
case object Duration extends Primitive[zio.Duration] {
final def parse(text: String): Either[Config.Error, zio.Duration] =
try {
Right(java.time.Duration.parse(text))
} catch {
case _: java.time.format.DateTimeParseException =>
try {
Right(zio.Duration.fromScala(scala.concurrent.duration.Duration(text)))
} catch {
case _: NumberFormatException =>
Left(Config.Error.InvalidData(Chunk.empty, s"Expected a duration value, but found ${text}"))
}
}
}
final case class Fail(message: String) extends Primitive[Nothing] {
final def parse(text: String): Either[Config.Error, Nothing] = Left(Config.Error.Unsupported(Chunk.empty, message))
}
sealed class Fallback[A] protected (val first: Config[A], val second: Config[A])
extends Composite[A]
with Product
with Serializable { self =>
def canEqual(that: Any): Boolean =
that.isInstanceOf[Fallback[_]]
def condition(error: Config.Error): Boolean =
true
override def equals(that: Any): Boolean =
that match {
case that: Fallback[_] => that.canEqual(self) && self.first == that.first && self.second == that.second
case _ => false
}
override def hashCode: Int =
(first, second).##
def productArity: Int =
2
def productElement(n: Int): Any =
n match {
case 0 => first
case 1 => second
case _ => throw new IndexOutOfBoundsException(n.toString)
}
}
object Fallback {
def apply[A](first: Config[A], second: Config[A]): Fallback[A] = new Fallback(first, second)
def unapply[A](fallback: Fallback[A]): Option[(Config[A], Config[A])] = Some((fallback.first, fallback.second))
}
final case class Optional[A](config: Config[A])
extends Fallback[Option[A]](config.map(Some(_)), Config.succeed(None)) {
override def condition(error: Config.Error): Boolean = error.isMissingDataOnly
}
final case class FallbackWith[A](
override val first: Config[A],
override val second: Config[A],
f: Config.Error => Boolean
) extends Fallback[A](first, second) {
override def condition(error: Config.Error): Boolean =
f(error)
}
case object Integer extends Primitive[BigInt] {
final def parse(text: String): Either[Config.Error, BigInt] = try Right(BigInt(text))
catch {
case NonFatal(e) => Left(Config.Error.InvalidData(Chunk.empty, s"Expected an integer value, but found ${text}"))
}
}
final case class Described[A](config: Config[A], description: String) extends Composite[A]
final case class Lazy[A](thunk: () => Config[A]) extends Composite[A]
case object LocalDateTime extends Primitive[java.time.LocalDateTime] {
final def parse(text: String): Either[Config.Error, java.time.LocalDateTime] = try Right(
java.time.LocalDateTime.parse(text)
)
catch {
case NonFatal(e) =>
Left(Config.Error.InvalidData(Chunk.empty, s"Expected a local date-time value, but found ${text}"))
}
}
case object LocalDate extends Primitive[java.time.LocalDate] {
final def parse(text: String): Either[Config.Error, java.time.LocalDate] = try Right(
java.time.LocalDate.parse(text)
)
catch {
case NonFatal(e) => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a local date value, but found ${text}"))
}
}
case object LocalTime extends Primitive[java.time.LocalTime] {
final def parse(text: String): Either[Config.Error, java.time.LocalTime] = try Right(
java.time.LocalTime.parse(text)
)
catch {
case NonFatal(e) => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a local time value, but found ${text}"))
}
}
final case class MapOrFail[A, B](original: Config[A], mapOrFail: A => Either[Config.Error, B]) extends Composite[B]
final case class Nested[A](name: String, config: Config[A]) extends Composite[A]
case object OffsetDateTime extends Primitive[java.time.OffsetDateTime] {
final def parse(text: String): Either[Config.Error, java.time.OffsetDateTime] = try Right(
java.time.OffsetDateTime.parse(text)
)
catch {
case NonFatal(e) =>
Left(Config.Error.InvalidData(Chunk.empty, s"Expected an offset date-time value, but found ${text}"))
}
}
case object SecretType extends Primitive[Secret] {
final def parse(text: String): Either[Config.Error, Secret] = Right(
Secret(text)
)
}
final case class Sequence[A](config: Config[A]) extends Composite[Chunk[A]]
final case class Switch[A, B](config: Config[A], map: Map[A, Config[B]]) extends Composite[B]
final case class Table[V](valueConfig: Config[V]) extends Composite[Map[String, V]]
case object Text extends Primitive[String] {
final def parse(text: String): Either[Config.Error, String] = Right(text)
}
final case class Zipped[A, B, C](left: Config[A], right: Config[B], zippable: Zippable.Out[A, B, C])
extends Composite[C]
/**
* The possible ways that loading configuration data may fail.
*/
sealed trait Error extends Exception with NoStackTrace { self =>
import Error._
def &&(that: Error): Error = And(self, that)
def ||(that: Error): Error = Or(self, that)
final def fold[Z](
invalidDataCase0: (Chunk[String], String) => Z,
missingDataCase0: (Chunk[String], String) => Z,
sourceUnavailableCase0: (Chunk[String], String, Cause[Throwable]) => Z,
unsupportedCase0: (Chunk[String], String) => Z
)(
andCase0: (Z, Z) => Z,
orCase0: (Z, Z) => Z
): Z =
foldContext(()) {
new Folder[Unit, Z] {
def invalidDataCase(context: Unit, path: Chunk[String], message: String): Z =
invalidDataCase0(path, message)
def missingDataCase(context: Unit, path: Chunk[String], message: String): Z =
missingDataCase0(path, message)
def sourceUnavailableCase(context: Unit, path: Chunk[String], message: String, cause: Cause[Throwable]): Z =
sourceUnavailableCase0(path, message, cause)
def unsupportedCase(context: Unit, path: Chunk[String], message: String): Z =
unsupportedCase0(path, message)
def andCase(context: Unit, left: Z, right: Z): Z =
andCase0(left, right)
def orCase(context: Unit, left: Z, right: Z): Z =
orCase0(left, right)
}
}
final def foldContext[C, Z](context: C)(folder: Folder[C, Z]): Z = {
import folder._
sealed trait ErrorCase
case object AndCase extends ErrorCase
case object OrCase extends ErrorCase
@tailrec
def loop(in: List[Error], out: List[Either[ErrorCase, Z]]): List[Z] =
in match {
case InvalidData(path, message) :: errors =>
loop(errors, Right(invalidDataCase(context, path, message)) :: out)
case MissingData(path, message) :: errors =>
loop(errors, Right(missingDataCase(context, path, message)) :: out)
case SourceUnavailable(path, message, cause) :: errors =>
loop(errors, Right(sourceUnavailableCase(context, path, message, cause)) :: out)
case Unsupported(path, message) :: errors =>
loop(errors, Right(unsupportedCase(context, path, message)) :: out)
case And(left, right) :: errors =>
loop(left :: right :: errors, Left(AndCase) :: out)
case Or(left, right) :: errors =>
loop(left :: right :: errors, Left(OrCase) :: out)
case Nil =>
out.foldLeft[List[Z]](List.empty) {
case (acc, Right(errors)) => errors :: acc
case (acc, Left(AndCase)) =>
val left :: right :: errors = (acc: @unchecked)
andCase(context, left, right) :: errors
case (acc, Left(OrCase)) =>
val left :: right :: errors = (acc: @unchecked)
orCase(context, left, right) :: errors
}
}
loop(List(self), List.empty).head
}
final def isMissingDataOnly: Boolean =
foldContext(())(Folder.IsMissingDataOnly)
def prefixed(prefix: Chunk[String]): Error
override def getMessage(): String = toString()
}
object Error {
final case class And(left: Error, right: Error) extends Error {
def prefixed(prefix: Chunk[String]): And =
copy(left = left.prefixed(prefix), right = right.prefixed(prefix))
override def toString(): String = s"(${left.toString()} and ${right.toString()})"
}
final case class InvalidData(path: Chunk[String] = Chunk.empty, message: String) extends Error {
def prefixed(prefix: Chunk[String]): InvalidData = copy(path = prefix ++ path)
override def toString(): String = s"(Invalid data at ${path.mkString(".")}: ${message})"
}
final case class MissingData(path: Chunk[String] = Chunk.empty, message: String) extends Error {
def prefixed(prefix: Chunk[String]): MissingData = copy(path = prefix ++ path)
override def toString(): String = s"(Missing data at ${path.mkString(".")}: ${message})"
}
final case class Or(left: Error, right: Error) extends Error {
def prefixed(prefix: Chunk[String]): Or =
copy(left = left.prefixed(prefix), right = right.prefixed(prefix))
override def toString(): String = s"(${left.toString()} or ${right.toString()})"
}
final case class SourceUnavailable(path: Chunk[String] = Chunk.empty, message: String, cause: Cause[Throwable])
extends Error {
def prefixed(prefix: Chunk[String]): SourceUnavailable = copy(path = prefix ++ path)
override def toString(): String = s"(Source unavailable at ${path.mkString(".")}: ${message})"
}
final case class Unsupported(path: Chunk[String] = Chunk.empty, message: String) extends Error {
def prefixed(prefix: Chunk[String]): Unsupported = copy(path = prefix ++ path)
override def toString(): String = s"(Unsupported operation at ${path.mkString(".")}: ${message})"
}
trait Folder[-Context, Z] {
def andCase(context: Context, left: Z, right: Z): Z
def invalidDataCase(context: Context, path: Chunk[String], message: String): Z
def missingDataCase(context: Context, path: Chunk[String], message: String): Z
def orCase(context: Context, left: Z, right: Z): Z
def sourceUnavailableCase(context: Context, path: Chunk[String], message: String, cause: Cause[Throwable]): Z
def unsupportedCase(context: Context, path: Chunk[String], message: String): Z
}
object Folder {
case object IsMissingDataOnly extends Folder[Any, Boolean] {
def andCase(context: Any, left: Boolean, right: Boolean): Boolean = left && right
def invalidDataCase(context: Any, path: Chunk[String], message: String): Boolean = false
def missingDataCase(context: Any, path: Chunk[String], message: String): Boolean = true
def orCase(context: Any, left: Boolean, right: Boolean): Boolean = left && right
def sourceUnavailableCase(
context: Any,
path: Chunk[String],
message: String,
cause: Cause[Throwable]
): Boolean = false
def unsupportedCase(context: Any, path: Chunk[String], message: String): Boolean = false
}
}
}
def bigDecimal: Config[BigDecimal] = Decimal
def bigDecimal(name: String): Config[BigDecimal] = bigDecimal.nested(name)
def bigInt: Config[BigInt] = Integer
def bigInt(name: String): Config[BigInt] = bigInt.nested(name)
def boolean: Config[Boolean] = Bool
def boolean(name: String): Config[Boolean] = boolean.nested(name)
def chunkOf[A](config: Config[A]): Config[Chunk[A]] = Sequence(config)
def chunkOf[A](name: String, config: Config[A]): Config[Chunk[A]] = chunkOf(config).nested(name)
def defer[A](config: => Config[A]): Config[A] =
Lazy(() => config)
def double: Config[Double] = bigDecimal.map(_.toDouble)
def double(name: String): Config[Double] = double.nested(name)
def duration: Config[zio.Duration] = Duration
def duration(name: String): Config[zio.Duration] = duration.nested(name)
def fail(error: => String): Config[Nothing] = defer(Fail(error))
def float: Config[Float] = bigDecimal.map(_.toFloat)
def float(name: String): Config[Float] = float.nested(name)
def int: Config[Int] = bigInt.map(_.toInt)
def int(name: String): Config[Int] = int.nested(name)
def listOf[A](config: Config[A]): Config[List[A]] = chunkOf(config).map(_.toList)
def listOf[A](name: String, config: Config[A]): Config[List[A]] = listOf(config).nested(name)
def localDate: Config[java.time.LocalDate] = LocalDate
def localDate(name: String): Config[java.time.LocalDate] = localDate.nested(name)
def localDateTime: Config[java.time.LocalDateTime] = LocalDateTime
def localDateTime(name: String): Config[java.time.LocalDateTime] = localDateTime.nested(name)
def localTime: Config[java.time.LocalTime] = LocalTime
def localTime(name: String): Config[java.time.LocalTime] = localTime.nested(name)
def logLevel: Config[LogLevel] = Config.string.mapOrFail { value =>
val label = value.toUpperCase
LogLevel.levels.find(_.label == label) match {
case Some(v) => Right(v)
case None => Left(Config.Error.InvalidData(Chunk.empty, s"Expected a log level, but found ${value}"))
}
}
def logLevel(name: String): Config[LogLevel] = logLevel.nested(name)
def long: Config[Long] = bigInt.map(_.toLong)
def long(name: String): Config[Long] = long.nested(name)
def nonEmptyChunkOf[A](config: Config[A]): Config[NonEmptyChunk[A]] =
chunkOf(config).mapOrFail(
NonEmptyChunk
.fromChunk(_)
.toRight(Config.Error.InvalidData(message = "Expected a NonEmptyChunk, but found Chunk.Empty"))
)
def nonEmptyChunkOf[A](name: String, config: Config[A]): Config[NonEmptyChunk[A]] =
nonEmptyChunkOf(config).nested(name)
def offsetDateTime: Config[java.time.OffsetDateTime] = OffsetDateTime
def offsetDateTime(name: String): Config[java.time.OffsetDateTime] = offsetDateTime.nested(name)
def secret: Config[Secret] = SecretType
def secret(name: String): Config[Secret] = secret.nested(name)
def setOf[A](config: Config[A]): Config[Set[A]] = chunkOf(config).map(_.toSet)
def setOf[A](name: String, config: Config[A]): Config[Set[A]] = setOf(config).nested(name)
def string: Config[String] = Text
def string(name: String): Config[String] = string.nested(name)
def succeed[A](value: => A): Config[A] = defer(Constant(value))
def table[V](value: Config[V]): Config[Map[String, V]] = Table(value)
def table[V](name: String, value: Config[V]): Config[Map[String, V]] = table(value).nested(name)
def uri: Config[java.net.URI] = string.mapAttempt(java.net.URI.create(_))
def uri(name: String): Config[java.net.URI] = uri.nested(name)
def vectorOf[A](config: Config[A]): Config[Vector[A]] = chunkOf(config).map(_.toVector)
def vectorOf[A](name: String, config: Config[A]): Config[Vector[A]] = vectorOf(config).nested(name)
final class OrElse[+A](self: Config[A], condition: Error => Boolean) {
def apply[A1 >: A](that: Config[A1]): Config[A1] =
FallbackWith(self, that, condition)
}
}