Metaconfig

Metaconfig

  • Docs
  • GitHub
Edit

Reference

Metaconfig is a library to read HOCON configuration into Scala case classes. Key features of Metaconfig include

  • helpful error messages on common mistakes like typos or type mismatch (expected string, obtained int)
  • configurable, semi-automatic derivation of decoders, with support for deprecating setting options
  • cross-platform, supports JS/JVM. Native support is on the roadmap

The target use-case for metaconfig is tool maintainers who support HOCON configuration in their tool. Metaconfig is used by scalafmt to read .scalafmt.conf and scalafix to read .scalafix.conf. With metaconfig, tool maintainers should be able to safely evolve their configuration (deprecate old fields, add new fields) without breaking existing configuration files. Users should get helpful error messages when they mistype a setting name.

There are alternatives to metaconfig that you might want to give a try first

  • https://github.com/circe/circe-config
  • https://github.com/pureconfig/pureconfig

Getting started

libraryDependencies += "com.geirsson" %% "metaconfig-core" % "0.10.0"

// Use https://github.com/lightbend/config to parse HOCON
libraryDependencies += "com.geirsson" %% "metaconfig-typesafe-config" % "0.10.0"

Use this import to access the metaconfig API

import metaconfig._

All of the following code examples assume that you have import metaconfig._ in scope.

Conf

Conf is a JSON-like data structure that is the foundation of metaconfig.

val string = Conf.fromString("string")
// string: Conf = Str(value = "string")
val int = Conf.fromInt(42)
// int: Conf = Num(value = 42)
Conf.fromList(int :: string :: Nil)
// res0: Conf = Lst(values = List(Num(value = 42), Str(value = "string")))
Conf.fromMap(Map("a" -> string, "b" -> int))
// res1: Conf = Obj(
//   values = List(("a", Str(value = "string")), ("b", Num(value = 42)))
// )

Conf.parse

You need an implicit MetaconfigParser to convert HOCON into Conf. Assuming you depend on the metaconfig-typesafe-config module,

import metaconfig.typesafeconfig._
Conf.parseString("""
a.b.c = 2
a.d = [ 1, 2, 3 ]
reference = ${a}
""")
// res2: Configured[Conf] = Ok(
//   value = Obj(
//     values = List(
//       (
//         "a",
//         Obj(
//           values = List(
//             (
//               "d",
//               Lst(values = List(Num(value = 1), Num(value = 2), Num(value = 3)))
//             ),
//             ("b", Obj(values = List(("c", Num(value = 2)))))
//           )
//         )
//       ),
//       (
//         "reference",
//         Obj(
//           values = List(
//             (
//               "d",
//               Lst(values = List(Num(value = 1), Num(value = 2), Num(value = 3)))
//             ),
//             ("b", Obj(values = List(("c", Num(value = 2)))))
//           )
//         )
//       )
//     )
//   )
// )
Conf.parseFile(new java.io.File(".scalafmt.conf"))
// res3: Configured[Conf] = Ok(
//   value = Obj(
//     values = List(
//       ("assumeStandardLibraryStripMargin", Bool(value = true)),
//       (
//         "project",
//         Obj(
//           values = List(
//             ("git", Bool(value = true)),
//             (
//               "excludeFilters",
//               Lst(
//                 values = List(
//                   Str(value = "(.*)/src/main/scala-3/(.*)"),
//                   Str(value = "(.*)/src/test/scala-3/(.*)")
//                 )
//               )
//             )
//           )
//         )
//       ),
//       ("align", Str(value = "none")),
//       ("version", Str(value = "2.4.2"))
//     )
//   )
// )

Note. The example above is JVM-only. For a Scala.js alternative, depend on the metaconfig-sconfig module and replace metaconfig.typesafeconfig with

import metaconfig.sconfig._

Conf.printHocon

It's possible to print Conf as HOCON.

Conf.printHocon(Conf.Obj(
  "a" -> Conf.Obj(
    "b" -> Conf.Str("3"),
    "c" -> Conf.Num(1),
    "d" -> Conf.Lst(
      Conf.Null(),
      Conf.Bool(true)
))))
// res4: String = """a.b = "3"
// a.c = 1
// a.d = [
//   null
//   true
// ]"""

The printer is tested against the roundtrip property

parse(print(conf)) == conf

so it should be safe to parse the output from the printer.

Conf.patch

Imagine the scenario

  • your application has many configuration options with default values,
  • you have a custom configuration object that overrides only a few specific fields.
  • you want to pretty-print the minimal HOCON configuration to obtain that custom configuration

Use Conf.patch compute a minimal Conf to go from an original Conf to a revised Conf.

val original = Conf.Obj(
  "a" -> Conf.Obj(
    "b" -> Conf.Str("c"),
    "d" -> Conf.Str("e")
  ),
  "f" -> Conf.Bool(true)
)
// original: Conf.Obj = Obj(
//   values = List(
//     ("a", Obj(values = List(("b", Str(value = "c")), ("d", Str(value = "e"))))),
//     ("f", Bool(value = true))
//   )
// )
val revised = Conf.Obj(
  "a" -> Conf.Obj(
    "b" -> Conf.Str("c"),
    "d" -> Conf.Str("ee") // <-- only overridden setting
  ),
  "f" -> Conf.Bool(true)
)
// revised: Conf.Obj = Obj(
//   values = List(
//     ("a", Obj(values = List(("b", Str(value = "c")), ("d", Str(value = "ee"))))),
//     ("f", Bool(value = true))
//   )
// )
val patch = Conf.patch(original, revised)
// patch: Conf = Obj(
//   values = List(("a", Obj(values = List(("d", Str(value = "ee"))))))
// )
Conf.printHocon(patch)
// res5: String = "a.d = ee"
val revised2 = Conf.applyPatch(original, patch)
// revised2: Conf = Obj(
//   values = List(
//     ("f", Bool(value = true)),
//     ("a", Obj(values = List(("b", Str(value = "c")), ("d", Str(value = "ee")))))
//   )
// )
assert(revised == revised2)

The patch operation is tested against the property

applyPatch(original, revised) == applyPatch(original, patch(original, revised))

ConfDecoder

To convert Conf into higher-level data structures you need a ConfDecoder[T] instance. Convert a partial function from Conf to your target type using ConfDecoder.fromPartial[T].

val number2 = ConfDecoder.fromPartial[Int]("String") {
    case Conf.Str("2") => Configured.Ok(2)
}
number2.read(Conf.fromString("2"))
// res7: Configured[Int] = Ok(value = 2)
number2.read(Conf.fromInt(2))
// res8: Configured[Int] = NotOk(
//   error = Type mismatch;
//   found    : Number (value: 2)
//   expected : String
// )

Convert a regular function from Conf to your target type using ConfDecoder.from[T].

case class User(name: String, age: Int)
val decoder = ConfDecoder.from[User] { conf =>
  conf.get[String]("name").product(conf.get[Int]("age")).map {
      case (name, age) => User(name, age)
  }
}
decoder.read(Conf.parseString("""
name = "Susan"
age = 29
"""))
// res9: Configured[User] = Ok(value = User(name = "Susan", age = 29))
decoder.read(Conf.parseString("""
name = 42
age = "Susan"
"""))
// res10: Configured[User] = NotOk(
//   error = 2 errors
// [E0] <input>:2:0 error: Type mismatch;
//   found    : Number (value: 42)
//   expected : String
// name = 42
// ^
// 
// [E1] <input>:3:0 error: Type mismatch;
//   found    : String (value: "Susan")
//   expected : Number
// age = "Susan"
// ^
// 
// 
// )

You can also use existing decoders to build more complex decoders

val fileDecoder = ConfDecoder.stringConfDecoder.flatMap { string =>
  val file = new java.io.File(string)
  if (file.exists()) Configured.ok(file)
  else ConfError.fileDoesNotExist(file).notOk
}
// fileDecoder: ConfDecoder[java.io.File] = metaconfig.ConfDecoder$$anonfun$flatMap$2@32899091
fileDecoder.read(Conf.fromString(".scalafmt.conf"))
// res11: Configured[java.io.File] = Ok(value = .scalafmt.conf)
fileDecoder.read(Conf.fromString(".foobar"))
// res12: Configured[java.io.File] = NotOk(
//   error = File /home/runner/work/metaconfig/metaconfig/.foobar does not exist.
// )

ConfDecoderEx and ConfDecoderExT

Similar to ConfDecoder but its read method takes an initial state as a parameter rather than as part of the decoder instance definition. ConfDecoderEx[A] is an alias for ConfDecoderExT[A, A].

Decoding collections

If a decoder for type T is defined, the package defines implicits to derive decoders for Option[T], Seq[T] and Map[String, T].

There's also a special for extending collections rather than redefining them (works only for ConfDecoderEx, not the original ConfDecoder):

// sets list
a = [ ... ]
// sets map
a = {
  b { ... }
  c { ... }
}

// extends list
a = {
  // must be the only key
  "+" = [ ... ]
}
// extends map
a = {
  // must be the only key
  "+" = {
    d { ... }
  }
}

ConfEncoder

To convert a class instance into Conf use ConfEncoder[T]. It's possible to automatically derive a ConfEncoder[T] instance for any case class with generic.deriveEncoder.

implicit val encoder = generic.deriveEncoder[User]
// encoder: ConfEncoder[User] = repl.MdocSession$App$$anon$1@49a64231

ConfEncoder[User].write(User("John", 42))
// res13: Conf = Obj(
//   values = List(("name", Str(value = "John")), ("age", Num(value = 42)))
// )

It's possible to compose ConfEncoder instances with contramap

val ageEncoder = ConfEncoder.IntEncoder.contramap[User](user => user.age)
ageEncoder.write(User("Ignored", 88))

ConfCodec

It's common to have a class that has both a ConfDecoder[T] and ConfEncoder[T] instance. For convenience, it's possible to use the ConfCodec[T] typeclass to wrap an encoder and decoder in one instance.

case class Bijective(name: String)
implicit val surface = generic.deriveSurface[Bijective]
implicit val codec = generic.deriveCodec[Bijective](new Bijective("default"))
ConfEncoder[Bijective].write(Bijective("John"))
// res14: Conf = Obj(values = List(("name", Str(value = "John"))))
ConfDecoder[Bijective].read(Conf.Obj("name" -> Conf.Str("Susan")))
// res15: Configured[Bijective] = Ok(value = Bijective(name = "Susan"))

It's possible to compose ConfCodec instances with bimap

val bijectiveString = ConfCodec.StringCodec.bimap[Bijective](_.name, Bijective(_))
bijectiveString.write(Bijective("write"))
// res16: Conf = Str(value = "write")
bijectiveString.read(Conf.Str("write"))
// res17: Configured[Bijective] = Ok(value = Bijective(name = "write"))

ConfCodecEx and ConfCodecExT

Similar to ConfCodec but derives from ConfDecoderExT instead of ConfDecoder.

ConfError

ConfError is a helper to produce readable and potentially aggregated error messages.

ConfError.message("Not good!")
// res18: ConfError = Not good!
ConfError.exception(new IllegalArgumentException("Expected String!"), stackSize = 2)
// res19: ConfError = java.lang.IllegalArgumentException: Expected String!
//  at repl.MdocSession$App.<init>(reference.md:206)
//  at repl.MdocSession$.app(reference.md:3)
// 
ConfError.typeMismatch("Int", "String", "field")
// res20: ConfError = Type mismatch at 'field';
//   found    : String
//   expected : Int
ConfError.message("Failure 1").combine(ConfError.message("Failure 2"))
// res21: ConfError = 2 errors
// [E0] Failure 1
// [E1] Failure 2
//

Metaconfig uses Input to represent a source that can be parsed and Position to represent range positions in a given Input

val input = Input.VirtualFile(
  "foo.scala",
  """
    |object A {
    |  var x
    |}
  """.stripMargin
)
val i = input.text.indexOf('v')
val pos = Position.Range(input, i, i)
ConfError.parseError(pos, "No var")
// res22: ConfError = foo.scala:3:2 error: No var
//   var x
//   ^
//

Configured

Configured[T] is like an Either[metaconfig.ConfError, T] which is used throughout the metaconfig API to either represent a successfully parsed/decoded value or a failure.

Configured.ok("Hello world!")
// res23: Configured[String] = Ok(value = "Hello world!")
Configured.ok(List(1, 2))
// res24: Configured[List[Int]] = Ok(value = List(1, 2))
val error = ConfError.message("Boom!")
// error: ConfError = Boom!
val configured = error.notOk
// configured: Configured[Nothing] = NotOk(error = Boom!)
configured.toEither
// res25: Either[ConfError, Nothing] = Left(value = Boom!)

To skip error handling, use the nuclear .get

configured.get
// java.util.NoSuchElementException: Boom!
//  at metaconfig.Configured.get(Configured.scala:15)
//  at repl.MdocSession$App$$anonfun$52.apply(reference.md:262)
//  at repl.MdocSession$App$$anonfun$52.apply(reference.md:262)
Configured.ok(42).get
// res26: Int = 42

generic.deriveSurface

To use automatic derivation, you first need a Surface[T] typeclass instance

import metaconfig.generic._
implicit val userSurface: Surface[User] =
  generic.deriveSurface[User]
// userSurface: Surface[User] = Surface(List(List(Field(name="name",tpe="String",annotations=List(),underlying=List()), Field(name="age",tpe="Int",annotations=List(),underlying=List()))))

The surface is used by metaconfig to support configurable decoding such as alternative fields names. In the future, the plan is to use Surface[T] to automatically generate html/markdown documentation for configuration settings. For now, you can ignore Surface[T] and just consider it as an annoying requirement from metaconfig.

generic.deriveDecoder

Writing manual decoder by hand grows tiring quickly. This becomes especially true when you have documentation to keep up-to-date as well.

implicit val decoder: ConfDecoder[User] =
  generic.deriveDecoder[User](User("John", 42)).noTypos
implicit val decoderEx: ConfDecoderEx[User] =
  generic.deriveDecoderEx[User](User("Jane", 24)).noTypos
ConfDecoder[User].read(Conf.parseString("""
name = Susan
age = 34
"""))
// res27: Configured[User] = Ok(value = User(name = "Susan", age = 34))
ConfDecoder[User].read(Conf.parseString("""
nam = John
age = 23
"""))
// res28: Configured[User] = NotOk(
//   error = <input>:2:0 error: found option 'nam' which wasn't expected, or isn't valid in this context.
//  Did you mean 'name'?
// nam = John
// ^
// 
// )
ConfDecoder[User].read(Conf.parseString("""
name = John
age = Old
"""))
// res29: Configured[User] = NotOk(
//   error = <input>:3:0 error: Type mismatch;
//   found    : String (value: "Old")
//   expected : Number
// age = Old
// ^
// 
// )
ConfDecoderEx[User].read(
  Some(User(name = "Jack", age = 33)),
  Conf.parseString("name = John")
)
// res30: Configured[User] = Ok(value = User(name = "John", age = 33))
ConfDecoderEx[User].read(
  None,
  Conf.parseString("name = John")
)
// res31: Configured[User] = Ok(value = User(name = "John", age = 24))

Sometimes automatic derivation fails, for example if your class contains fields that have no ConfDecoder instance

import java.io.File
case class Funky(file: File)
implicit val surface = generic.deriveSurface[Funky]
// surface: Surface[Funky] = Surface(List(List(Field(name="file",tpe="File",annotations=List(@TabCompleteAsPath()),underlying=List()))))

This will fail with a fail cryptic compile error

implicit val decoder = generic.deriveDecoder[Funky](Funky(new File("")))
// error: decoder is already defined as value decoder
// implicit val decoder = generic.deriveDecoder[Funky](Funky(new File("")))
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// error: could not find implicit value for parameter ev: metaconfig.ConfDecoder[repl.MdocSession.App.User]
// ConfDecoder[User].read(Conf.parseString("""
// ^^^^^^^^^^^^^^^^^
// error: could not find implicit value for parameter ev: metaconfig.ConfDecoder[repl.MdocSession.App.User]
// ConfDecoder[User].read(Conf.parseString("""
// ^^^^^^^^^^^^^^^^^
// error: could not find implicit value for parameter ev: metaconfig.ConfDecoder[repl.MdocSession.App.User]
// ConfDecoder[User].read(Conf.parseString("""
// ^^^^^^^^^^^^^^^^^
// error: could not find implicit value for parameter ev: metaconfig.ConfDecoder[java.io.File]
// implicit val decoder = generic.deriveDecoder[Funky](Funky(new File("")))
//                                                    ^

Observe that the error message is complaining about a missing metaconfig.ConfDecoder[java.io.File] implicit.

Limitations

The following features are not supported by generic derivation

  • derivation for objects, sealed traits or non-case classes, only case classes are supported
  • parameterized types, it's possible to derive decoders for a concrete parameterized type like Option[Foo] but note that the type field (Field.tpe) will be pretty-printed to the generic representation of that field: Option[T].value: T.

@DeprecatedName

As your configuration evolves, you may want to rename some settings but you have existing users who are using the old name. Use the @DeprecatedName annotation to continue supporting the old name even if you go ahead with the rename.

import metaconfig.annotation._
case class EvolvingConfig(
    @DeprecatedName("goodName", "Use isGoodName instead", "1.0")
    isGoodName: Boolean
)
implicit val surface = generic.deriveSurface[EvolvingConfig]
implicit val decoder = generic.deriveDecoder[EvolvingConfig](EvolvingConfig(true)).noTypos
decoder.read(Conf.Obj("goodName" -> Conf.fromBoolean(false)))
// res33: Configured[EvolvingConfig] = Ok(
//   value = EvolvingConfig(isGoodName = false)
// )
decoder.read(Conf.Obj("isGoodName" -> Conf.fromBoolean(false)))
// res34: Configured[EvolvingConfig] = Ok(
//   value = EvolvingConfig(isGoodName = false)
// )
decoder.read(Conf.Obj("gooodName" -> Conf.fromBoolean(false)))
// res35: Configured[EvolvingConfig] = NotOk(
//   error = found option 'gooodName' which wasn't expected, or isn't valid in this context.
//  Did you mean 'isGoodName'?
// )

Conf.parseCliArgs

Metaconfig can parse command line arguments into a Conf.

case class App(
  @Description("The directory to output files")
  target: String = "out",
  @Description("Print out debugging diagnostics")
  @ExtraName("v")
  verbose: Boolean = false,
  @Description("The input files for app")
  @ExtraName("remainingArgs")
  files: List[String] = Nil
)
implicit val surface = generic.deriveSurface[App]
implicit val codec = generic.deriveCodec[App](App())
val conf = Conf.parseCliArgs[App](List(
  "--verbose",
  "--target", "/tmp",
  "input.txt"
))
// conf: Configured[Conf] = Ok(
//   value = Obj(
//     values = List(
//       ("remainingArgs", Lst(values = List(Str(value = "input.txt")))),
//       ("target", Str(value = "/tmp")),
//       ("verbose", Bool(value = true))
//     )
//   )
// )

Decode the cli args into App like normal

val app = decoder.read(conf.get)
// app: Configured[EvolvingConfig] = NotOk(
//   error = 3 errors
// [E0] found option 'remainingArgs' which wasn't expected, or isn't valid in this context.
// [E1] found option 'target' which wasn't expected, or isn't valid in this context.
// [E2] found option 'verbose' which wasn't expected, or isn't valid in this context.
// 
// )

Settings.toCliHelp

Generate a --help message with a Settings[T].

Settings[App].toCliHelp(default = App())
// res36: String = """--target: String = "out"   The directory to output files
// --verbose: Boolean = false Print out debugging diagnostics
// --files: List[String] = [] The input files for app"""

@Inline

If you have multiple cli apps that all share a base set of fields you can use @Inline.

case class Common(
  @Description("The working directory")
  cwd: String = "",
  @Description("The output directory")
  out: String = ""
)
implicit val surface = generic.deriveSurface[Common]
implicit val codec = generic.deriveCodec[Common](Common())

case class AgeApp(
  @Description("The user's age")
  age: Int = 0,
  @Inline
  common: Common = Common()
)
implicit val ageSurface = generic.deriveSurface[AgeApp]
implicit val ageCodec = generic.deriveCodec[AgeApp](AgeApp())

case class NameApp(
  @Description("The user's name")
  name: String = "John",
  @Inline
  common: Common = Common()
)
implicit val nameSurface = generic.deriveSurface[NameApp]
implicit val nameCodec = generic.deriveCodec[NameApp](NameApp())

Observe that NameApp and AgeApp both have an @Inline common: Common field. It is not necessary to prefix cli args with the name of @Inline fields. In the example above, it's possible to pass in --out target instead of --common.out target to override the common output directory.

Conf.parseCliArgs[NameApp](List("--out", "/tmp", "--cwd", "working-dir"))
// res37: Configured[Conf] = Ok(
//   value = Obj(
//     values = List(
//       (
//         "common",
//         Obj(
//           values = List(
//             ("cwd", Str(value = "working-dir")),
//             ("out", Str(value = "/tmp"))
//           )
//         )
//       )
//     )
//   )
// )
val conf = Conf.parseCliArgs[AgeApp](List("--out", "target", "--cwd", "working-dir"))
// conf: Configured[Conf] = Ok(
//   value = Obj(
//     values = List(
//       (
//         "common",
//         Obj(
//           values = List(
//             ("cwd", Str(value = "working-dir")),
//             ("out", Str(value = "target"))
//           )
//         )
//       )
//     )
//   )
// )
conf.get.as[AgeApp].get
// res38: AgeApp = AgeApp(
//   age = 0,
//   common = Common(cwd = "working-dir", out = "target")
// )

The generated --help message does not display @Inline fields. Instead, the nested fields inside the type of the @Inline field are shown in the --help message.

Settings[NameApp].toCliHelp(default = NameApp())
// res39: String = """--name: String = "John" The user's name
// --cwd: String = ""      The working directory
// --out: String = ""      The output directory"""

Docs

To generate documentation for you configuration, add a dependency to the following module

libraryDependencies += "com.geirsson" %% "metaconfig-docs" % "0.10.0"

First define your configuration

import metaconfig._
import metaconfig.annotation._
import metaconfig.generic._

case class Home(
    @Description("Address description")
    address: String = "Lakelands 2",
    @Description("Country description")
    country: String = "Iceland"
)
implicit val homeSurface = generic.deriveSurface[Home]
implicit val homeEncoder = generic.deriveEncoder[Home]

case class User(
    @Description("Name description")
    name: String = "John",
    @Description("Age description")
    age: Int = 42,
    home: Home = Home()
)
implicit val userSurface = generic.deriveSurface[User]
implicit val userEncoder = generic.deriveEncoder[User]

To generate html documentation, pass in a default value

docs.Docs.html(User())
// res41: String = "<table><thead><tr><th>Name</th><th>Type</th><th>Description</th><th>Default value</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>String</code></td><td>Name description</td><td>&quot;John&quot;</td></tr><tr><td><code>age</code></td><td><code>Int</code></td><td>Age description</td><td>42</td></tr><tr><td><code>home.address</code></td><td><code>String</code></td><td>Address description</td><td>&quot;Lakelands 2&quot;</td></tr><tr><td><code>home.country</code></td><td><code>String</code></td><td>Country description</td><td>&quot;Iceland&quot;</td></tr></tbody></table>"

The output will look like this when rendered in a markdown or html document

Name Type Description Default value
name String Name description "John"
age Int Age description 42
home.address String Address description "Lakelands 2"
home.country String Country description "Iceland"

The Docs.html method does nothing magical, it's possible to implement custom renderings by inspecting Settings[T] directly.

Settings[User].settings
// res43: List[Setting] = List(
//   Setting(Field(name="name",tpe="String",annotations=List(@Description(Name description)),underlying=List())),
//   Setting(Field(name="age",tpe="Int",annotations=List(@Description(Age description)),underlying=List())),
//   Setting(Field(name="home",tpe="Home",annotations=List(),underlying=List(List(Field(name="address",tpe="String",annotations=List(@Description(Address description)),underlying=List()), Field(name="country",tpe="String",annotations=List(@Description(Country description)),underlying=List())))))
// )
val flat = Settings[User].flat(ConfEncoder[User].writeObj(User()))
// flat: List[(Setting, Conf)] = List(
//   (
//     Setting(Field(name="name",tpe="String",annotations=List(@Description(Name description)),underlying=List())),
//     Str(value = "John")
//   ),
//   (
//     Setting(Field(name="age",tpe="Int",annotations=List(@Description(Age description)),underlying=List())),
//     Num(value = 42)
//   ),
//   (
//     Setting(Field(name="home.address",tpe="String",annotations=List(@Description(Address description)),underlying=List())),
//     Str(value = "Lakelands 2")
//   ),
//   (
//     Setting(Field(name="home.country",tpe="String",annotations=List(@Description(Country description)),underlying=List())),
//     Str(value = "Iceland")
//   )
// )
flat.map { case (setting, defaultValue) =>
  s"Setting ${setting.name} of type ${setting.tpe} has default value $defaultValue"
}.mkString("\n==============\n")
// res44: String = """Setting name of type String has default value "John"
// ==============
// Setting age of type Int has default value 42
// ==============
// Setting home.address of type String has default value "Lakelands 2"
// ==============
// Setting home.country of type String has default value "Iceland""""
  • Getting started
  • Conf
  • Conf.parse
  • Conf.printHocon
  • Conf.patch
  • ConfDecoder
  • ConfDecoderEx and ConfDecoderExT
    • Decoding collections
  • ConfEncoder
  • ConfCodec
  • ConfCodecEx and ConfCodecExT
  • ConfError
  • Configured
  • generic.deriveSurface
  • generic.deriveDecoder
    • Limitations
  • @DeprecatedName
  • Conf.parseCliArgs
  • Settings.toCliHelp
  • @Inline
  • Docs
Metaconfig
Metaconfig Docs
Getting started
Community
Copyright © 2022 Scalameta