{"id":13428888,"url":"https://github.com/sksamuel/hoplite","last_synced_at":"2025-05-14T08:07:50.157Z","repository":{"id":35039054,"uuid":"187727131","full_name":"sksamuel/hoplite","owner":"sksamuel","description":"A boilerplate-free Kotlin config library for loading configuration files as data classes","archived":false,"fork":false,"pushed_at":"2025-03-31T03:37:50.000Z","size":1973,"stargazers_count":967,"open_issues_count":34,"forks_count":85,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-05-13T06:49:38.363Z","etag":null,"topics":["config","config-loader","configuration","kotlin","library","yaml"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sksamuel.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-05-20T23:27:42.000Z","updated_at":"2025-05-12T19:37:44.000Z","dependencies_parsed_at":"2023-02-17T23:31:33.352Z","dependency_job_id":"e9c78705-e845-4297-b6cf-175f447ffae5","html_url":"https://github.com/sksamuel/hoplite","commit_stats":{"total_commits":890,"total_committers":53,"mean_commits":16.79245283018868,"dds":"0.16966292134831462","last_synced_commit":"1c698e5718269773f4b6cf17eb43dc73fe853fd2"},"previous_names":[],"tags_count":91,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Fhoplite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Fhoplite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Fhoplite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sksamuel%2Fhoplite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sksamuel","download_url":"https://codeload.github.com/sksamuel/hoplite/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254101557,"owners_count":22014908,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["config","config-loader","configuration","kotlin","library","yaml"],"created_at":"2024-07-31T01:01:07.896Z","updated_at":"2025-05-14T08:07:45.133Z","avatar_url":"https://github.com/sksamuel.png","language":"Kotlin","funding_links":[],"categories":["Libraries","Ktor Projects","config"],"sub_categories":["Socials"],"readme":"# Hoplite \u003cimg src=\"logo.png\" height=40\u003e\n\nHoplite is a Kotlin library for loading configuration files into typesafe classes in a boilerplate-free way.\nDefine your config using Kotlin data classes, and at startup Hoplite will read from one or more config files,\nmapping the values in those files into your config classes. Any missing values, or values that cannot be converted\ninto the required type will cause the config to fail with detailed error messages.\n\n![master](https://github.com/sksamuel/hoplite/workflows/master/badge.svg)\n[\u003cimg src=\"https://img.shields.io/maven-central/v/com.sksamuel.hoplite/hoplite-core.svg?label=latest%20release\"/\u003e](http://search.maven.org/#search%7Cga%7C1%7Choplite)\n[\u003cimg src=\"https://img.shields.io/nexus/s/https/s01.oss.sonatype.org/com.sksamuel.hoplite/hoplite-core.svg?label=latest%20snapshot\u0026style=plastic\"/\u003e](https://s01.oss.sonatype.org/content/repositories/snapshots/com/sksamuel/hoplite/)\n\n## Features\n\n- **Multiple formats:** Write your configuration in [several formats](#supported-formats): Yaml, JSON, Toml, Hocon, or\n  Java .props files or even mix and match formats in the same system.\n- **Property Sources:** Per-system overrides are possible from JVM system properties,\n  environment variables, JNDI or a per-user local config file.\n- **Batteries included:** Support for many [standard types](#decoders) such as primitives, enums, dates, collection\n  types, inline classes, uuids, nullable types, as well as popular Kotlin third party library types such\n  as `NonEmptyList`, `Option` and `TupleX` from [Arrow](https://arrow-kt.io/).\n- **Custom Data Types:** The `Decoder` interface makes it easy to add support for your custom domain types or standard\n  library types not covered out of the box.\n- **Cascading:** Config files can be [stacked](#cascading-config). Start with a default file and then layer new\n  configurations on top. When resolving config, lookup of values falls through to the first file that contains a\n  definition. Can be used to have a default config file and then an environment specific file.\n- **Beautiful errors:** Fail fast at runtime, with [beautiful errors](#beautiful-errors) showing exactly what went wrong and where.\n- **Preprocessors:** Support for several [preprocessors](https://github.com/sksamuel/hoplite#preprocessors) that will\n  replace placeholders with values resolved from external configs, such as AWS Secrets Manager, Azure KeyVault and so on.\n- **Reloadable config:** Trigger config [reloads](#reloadable-config) on a fixed interval or in response to external\n  events such as consul value changes.\n- **Prefix Binding:** Optionally, load configuration sources once, and then [bind individual prefix](#prefix-binding)\n  paths into independent config types.\n\n## Changelog\n\nSee the list of changes in each release [here](changelog.md).\n\n## Getting Started\n\nAdd Hoplite to your build:\n\n```groovy\nimplementation 'com.sksamuel.hoplite:hoplite-core:\u003cversion\u003e'\n```\n\nYou will also need to include a module for the [format(s)](#supported-formats) you to use.\n\nNext define the data classes that are going to contain the config.\nYou should create a top level class which can be named simply Config, or ProjectNameConfig. This class then defines a field for each config value you need. It can include nested data classes for grouping together related configs.\n\nFor example, if we had a project that needed database config, config for an embedded HTTP server, and a field which contained which environment we were running in (staging, QA, production etc), then we may define our classes like this:\n\n```kotlin\ndata class Database(val host: String, val port: Int, val user: String, val pass: String)\ndata class Server(val port: Int, val redirectUrl: String)\ndata class Config(val env: String, val database: Database, val server: Server)\n```\n\nFor our staging environment, we may create a YAML (or Json, etc) file called `application-staging.yaml`.\nThe name doesn't matter, you can use any convention you wish.\n\n```yaml\nenv: staging\n\ndatabase:\n  host: staging.wibble.com\n  port: 3306\n  user: theboss\n  pass: 0123abcd\n\nserver:\n  port: 8080\n  redirectUrl: /404.html\n```\n\nFinally, to build an instance of `Config` from this file, and assuming the config file was on the classpath, we can simply execute:\n\n```kotlin\nval config = ConfigLoaderBuilder.default()\n               .addResourceSource(\"/application-staging.yml\")\n               .build()\n               .loadConfigOrThrow\u003cConfig\u003e()\n```\n\nIf the values in the config file are compatible, then an instance of `Config` will be returned.\nOtherwise, an exception will be thrown containing details of the errors.\n\n\n\n\n## Config Loader\n\nAs you have seen from the getting started guide, `ConfigLoader` is the entry point to using Hoplite. We create an\ninstance of this loader class through the `ConfigLoaderBuilder` builder. To this builder we add sources, configuration,\nenable reports, add preprocessors and more.\n\nTo create a default builder, use `ConfigLoaderBuilder.default()` and after adding your sources, call `build`.\nHere is an example:\n\n```kotlin\nConfigLoaderBuilder.default()\n  .addResourceSource(\"/application-prod.yml\")\n  .addResourceSource(\"/reference.json\")\n  .build()\n  .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\nThe `default` method on `ConfigLoaderBuilder` sets up recommended defaults. If you wish to start with a completely empty\nconfig builder, then use `ConfigLoaderBuilder.empty()`.\n\nThere are two ways to retrieve a populated data class from config. The first is to throw an exception if the config\ncould not be resolved. We do this via the `loadConfigOrThrow\u003cT\u003e` function. Another is to return a `ConfigResult` validation\nmonad via the `loadConfig\u003cT\u003e` function if you want to handle errors manually.\n\nFor most cases, when you are resolving config at application startup, the exception based approach is better. This is\nbecause you typically want any errors in config to abort application bootstrapping, dumping errors immediately to the console.\n\n### Prefix Binding\n\nPrefixes can be used to bind selected config to independent data classes. This is useful for modular config loading.\nFor example, independent modules or plugins load their own config from a common set of configuration sources.\n\nFor example a yaml source containing\n\n```yaml\nmodule1:\n  foo: bar\n\nmodule2:\n  baz: qux\n```\n\ncan be bound to:\n\n```kotlin\ndata class Module1Config(val foo: String)\n\ndata class Module2Config(val baz: String)\n```\n\nThe best way to do this is to obtain a `ConfigBinder` from `ConfigLoader`, for example:\n\n```kotlin\nval configBinder = ConfigLoaderBuilder.default()\n  .addResourceSource(\"/application-prod.yml\")\n  .addResourceSource(\"/reference.json\")\n  .build()\n  .configBinder()\n\n// generally a ConfigBinder will be provided via DI, and these calls will be in their own modules!\nval module1Config = configBinder.bindOrThrow\u003cModule1Config\u003e(\"module1\")\nval module2Config = configBinder.bindOrThrow\u003cModule2Config\u003e(\"module2\")\n```\n\nWith this approach, the configuration sources will only be read and parsed a single time, but can be bound to independent\ndata classes as many times as is necessary.\n\nA `prefix` can also be provided directly to `loadConfig` and its variants if only one prefix needs to be loaded.\n\nThe `prefix` value does not have to refer only to root properties -- a prefix of `foo.bar` will access config at the `foo.bar` node in the config tree that `ConfigLoader` creates.\n\n\n## Beautiful Errors\n\nWhen an error does occur, if you choose to throw an exception, the errors will be formatted in a human-readable way\nalong with as much location information as possible. No more trying to track down a `NumberFormatException` in a 400\nline config file.\n\nHere is an example of the error formatting for a test file used by the unit tests. Notice that the errors indicate which\nfile the value was pulled from.\n\n```\nError loading config because:\n\n    - Could not instantiate 'com.sksamuel.hoplite.json.Foo' because:\n\n        - 'bar': Required type Boolean could not be decoded from a Long (classpath:/error1.json:2:19)\n\n        - 'baz': Missing from config\n\n        - 'hostname': Type defined as not-null but null was loaded from config (classpath:/error1.json:6:18)\n\n        - 'season': Required a value for the Enum type com.sksamuel.hoplite.json.Season but given value was Fun (/home/user/default.json:8:18)\n\n        - 'users': Defined as a List but a Boolean cannot be converted to a collection (classpath:/error1.json:3:19)\n\n        - 'interval': Required type java.time.Duration could not be decoded from a String (classpath:/error1.json:7:26)\n\n        - 'nested': - Could not instantiate 'com.sksamuel.hoplite.json.Wibble' because:\n\n            - 'a': Required type java.time.LocalDateTime could not be decoded from a String (classpath:/error1.json:10:17)\n\n            - 'b': Unable to locate a decoder for java.time.LocalTime\n```\n\n## Supported Formats\n\nHoplite supports config files in several formats. You can mix and match formats if you really want to.\nFor each format you wish to use, you must include the appropriate hoplite module on your classpath.\nThe format that hoplite uses to parse a file is determined by the file extension.\n\n| Format                                                              | Module                                                             | File Extensions     |\n|:--------------------------------------------------------------------|:-------------------------------------------------------------------|:--------------------|\n| [Json](https://www.json.org/)                                       | [`hoplite-json`](https://search.maven.org/search?q=hoplite-json)   | .json               |\n| [Yaml](https://yaml.org/) Note: Yaml files are limited 3mb in size. | [`hoplite-yaml`](https://search.maven.org/search?q=hoplite-yaml)   | .yml, .yaml         |\n| [Toml](https://github.com/toml-lang/toml)                           | [`hoplite-toml`](https://search.maven.org/search?q=hoplite-toml)   | .toml               |\n| [Hocon](https://github.com/lightbend/config)                        | [`hoplite-hocon`](https://search.maven.org/search?q=hoplite-hocon) | .conf               |\n| Java Properties files                                               | built-in                                                           | .props, .properties |\n\nIf you wish to add another format you can extend `Parser` and provide an instance of that implementation to\nthe `ConfigLoaderBuilder` via `addParser`.\n\nThat same function can be used to map non-default file extensions to an existing parser. For example, if you wish to\nhave your config in files called `application.data` but in yaml format, then you can register .data with the Yaml parser\nlike this:\n\n`ConfigLoaderBuilder.default().addParser(\"data\", YamlParser).build()`\n\n### Note: fatJar/shadowJar\n\nIf attempting to build a \"fat Jar\" while using multiple file type modules, it is essential to use [the shadowJar plugin](https://imperceptiblethoughts.com/shadow/) and to add the directive `mergeServiceFiles()` in the shadowJar Gradle task. [More info](https://imperceptiblethoughts.com/shadow/configuration/merging/#merging-service-descriptor-files)\n\n## Property Sources\n\nThe `PropertySource` interface is how Hoplite reads configuration values.\n\nHoplite supports several built in property source implementations, and you can write your own if required.\n\nThe `EnvironmentVariablesPropertySource`, `SystemPropertiesPropertySource`, `UserSettingsPropertySource`, and `XdgConfigPropertySource`\nsources are automatically registered, with precedence in that order. Other property sources can be passed to the config loader builder\nas required.\n\n\n\n### EnvironmentVariablesPropertySource\n\nThe `EnvironmentVariablesPropertySource` reads config from environment variables.\nThis property source maps environment variable names to config properties via idiomatic conventions for environment variables.\nEnv vars are idiomatically UPPERCASE and [contain only](https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html) letters (`A` to `Z`), digits (`0` to `9`), and the underscore (`_`) character.\n\nHoplite maps env vars as follows:\n\n* Underscores are separators for nested config. For example `TOPIC_NAME` would override a property `name` located in a `topic` parent.\n\n* To bind env vars to arrays or lists, postfix with an index e.g. set env vars `TOPIC_NAME_0` and `TOPIC_NAME_1` to set two values for the `name` list property. Missing indices are ignored, which is useful for commenting out values without renumbering subsequent ones.\n\n* To bind env vars to maps, the key is part of the nested config e.g. `TOPIC_NAME_FOO` and `TOPIC_NAME_BAR` would set the \"foo\" and \"bar\"\nkeys for the `name` map property. Note that keys are one exception to the idiomatic uppercase rule -- the env var name determines the\ncase of the map key.\n\nIf the optional (not specified by default) `prefix` setting is provided, then only env vars that begin with the prefix are considered,\nand the prefix is stripped from the env var before processing.\n\nAs of Hoplite 3, the `EnvironmentVariablesPropertySource` is applied by default and may be used to override other config properties\ndirectly. There is no longer any built-in support for the `config.override.` prefix. However, the optional `prefix` setting can still\nbe used for the same purpose.\n\n\n### SystemPropertiesPropertySource\n\nThe `SystemPropertiesPropertySource` provides config through system properties that are prefixed with `config.override.`.\nFor example, starting your JVM with `-Dconfig.override.database.name` would override a config key of `database.name` residing in a file.\n\n\n### UserSettingsPropertySource\n\nThe `UserSettingsPropertySource` provides config through a config file defined at ~/.userconfig.[ext] where ext is one of the [supported formats](#supported-formats).\n\n\n### InputStreamPropertySource\n\nThe `InputStreamPropertySource` provides config from an input stream. This source requires a parameter that indicates what the format is. For example, `InputStreamPropertySource(input, \"yml\")`\n\n\n### ConfigFilePropertySource\n\nConfig from files or resources are retrieved via instances of `ConfigFilePropertySource`. This property source is added automatically when we pass\nstrings to the `loadConfigOrThrow` or `loadConfig` functions.\n\nThere are convenience methods on `ConfigLoaderBuilder` to construct `ConfigFilePropertySource`s from resources on the classpath or files.\n\nFor example, the following are equivalent:\n\n```kotlin\nConfigLoader().loadConfigOrThrow\u003cMyConfig\u003e(\"/config.json\")\n```\n\nand\n\n```kotlin\nConfigLoaderBuilder.default()\n   .addResourceSource(\"/config.json\")\n   .build()\n   .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\nThe advantage of the second approach is that we can specify a file can be optional, for example:\n\n```kotlin\nConfigLoaderBuilder.default()\n  .addResourceSource(\"/missing.yml\", optional = true)\n  .addResourceSource(\"/config.json\")\n  .build()\n  .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\n### JsonPropertySource\n\nTo use a JSON string as a property source, we can use the `JsonPropertySource` implementation.\nFor example,\n\n```kotlin\nConfigLoaderBuilder.default()\n   .addSource(JsonPropertySource(\"\"\" { \"database\": \"localhost\", \"port\": 1234 } \"\"\"))\n   .build()\n   .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\n### YamlPropertySource\n\nTo use a Yaml string as a property source, we can use the `YamlPropertySource` implementation.\n\n```kotlin\nConfigLoaderBuilder.default()\n   .addSource(YamlPropertySource(\n     \"\"\"\n        database: \"localhost\"\n        port: 1234\n     \"\"\"))\n   .build()\n   .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\n### TomlPropertySource\n\nTo use a Toml string as a property source, we can use the `TomlPropertySource` implementation.\n\n```kotlin\nConfigLoaderBuilder.default()\n  .addSource(TomlPropertySource(\n    \"\"\"\n        database = \"localhost\"\n        port = 1234\n     \"\"\"))\n  .build()\n  .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\n### PropsPropertySource\n\nTo use a java.util.Properties object as property source, we can use the `PropsPropertySource` implementation.\n\n```kotlin\nConfigLoaderBuilder.default()\n  .addSource(PropsPropertySource(myProps))\n  .build()\n  .loadConfigOrThrow\u003cMyConfig\u003e()\n```\n\n## Cascading Config\n\nHoplite has the concept of cascading or layered or fallback config.\nThis means you can pass more than one config file to the ConfigLoader.\nWhen the config is resolved into Kotlin classes, a lookup will cascade or fall through one file to another in the order they were passed to the loader, until the first file that defines that key.\n\nFor example, if you had the following two files in yaml:\n\n`application.yaml`:\n```yaml\nelasticsearch:\n  port: 9200\n  clusterName: product-search\n```\n\n`application-prod.yaml`:\n```yaml\nelasticsearch:\n  host: prd-elasticsearch.scv\n  port: 8200\n```\n\nAnd both were passed to the ConfigLoader like this: `ConfigLoader().loadConfigOrThrow\u003cConfig\u003e(\"/application-prod.yaml\", \"/application.yaml\")`, then lookups will be attempted in the order the files were declared.\nSo in this case, the config would be resolved like this:\n```\nelasticsearch.port = 8200 // the value in application-prod.yaml takes priority\nelasticsearch.host = prd-elasticsearch.scv // only defined in application-prod.yaml\nelasitcsearch.clusterName = product-search // only defined in application.yaml\n```\n\nLet's see a more complicated example. In JSON this time.\n\n`default.json`\n```json\n{\n  \"a\": \"alice\",\n  \"b\": {\n    \"c\": true,\n    \"d\": 123\n  },\n  \"e\": [\n    {\n      \"x\": 1,\n      \"y\": true\n    },\n    {\n      \"x\": 2,\n      \"y\": false\n    }\n  ],\n  \"f\": \"Fall\"\n}\n```\n\n`prod.json`\n```json\n{\n  \"a\": \"bob\",\n  \"b\": {\n    \"d\": 999\n  },\n  \"e\": [\n    {\n      \"y\": true\n    }\n  ]\n}\n```\n\nAnd we will parse the above config files into these data classes:\n\n```kotlin\nenum class Season { Fall, Winter, Spring, Summer }\ndata class Foo(val c: Boolean, val d: Int)\ndata class Bar(val x: Int?, val y: Boolean)\ndata class Config(val a: String, val b: Foo, val e: List\u003cBar\u003e, val f: Season)\n```\n\n```kotlin\nval config = ConfigLoader.load(\"prod.json\", \"default.json\")\nprintln(config)\n```\n\nThe resolution rules are as follows:\n\n- \"a\" is present in both files and so is resolved from the first file - which was \"prod.json\"\n- \"b\" is present in both files and therefore resolved from the file as well\n- \"c\" is a nested value of \"b\" and is not present in the first file so is resolved from the second file \"default.json\"\n- \"d\" is a nested value of \"b\" present in both files and therefore resolved from the first file\n- \"e\" is present in both files and so the entire list is resolved from the first file. This means that the list only contains a single element, and x is null despite being present in the list in the first file. List's cannot be merged.\n- \"f\" is only present in the second file and so is resolved from the second file.\n\n\n\n## Strict Mode\n\nHoplite can be configured to throw an error if a config value is not used.\nThis is useful to detect stale configs.\n\nTo enable this setting, use `.strict()` on the config builder. For example:\n\n```kotlin\nConfigLoaderBuilder.default()\n  .addResourceSource(\"/config-prd.yml\", true)\n  .addResourceSource(\"/config.yml\")\n  .strict()\n  .build()\n  .loadConfig\u003cMyConfig\u003e()\n```\n\nAn example of this output is:\n\n```\nError loading config because:\n\n    Config value 'drop_drop' at (classpath:/snake_case.yml:0:10) was unused\n\n    Config value 'double_trouble' at (/home/sam/.userconfig.yml:2:16) was unused\n```\n\n## Aliases\n\nIf you wish to refactor your config classes and rename a field, but you don't want to have to update all your config files,\nyou can add a migration path by allowing a field to use more than one name. To do this we use the @ConfigAlias annotation.\n\nFor example, with this config file:\n\n```yml\ndatabase:\n  host: String\n```\n\nWe can marshall this into the following data classes.\n\n```kotlin\ndata class Database(val host: String)\ndata class MyConfig(val database: Database)\n```\n\nor\n\n```kotlin\ndata class Database(@ConfigAlias(\"host\") val hostname: String)\ndata class MyConfig(val database: Database)\n```\n\n\n## Param Mappers\n\nHoplite provides an interface `ParameterMapper` which allows the parameter name to be modified before it is looked up\ninside a config source. This allows hoplite to find config keys which don't match the exact name. The main use case for\nthis is to allow `snake_case` or `kebab-case` names to be used as config keys.\n\nFor example, given the following config class:\n\n```kotlin\ndata class Database(val instanceHostName: String)\n```\n\nThen we can of course define our config file (using YML as an example):\n\n```yml\ndatabase:\n    instanceHostName: server1.prd\n```\n\nBut because Hoplite registers `KebabCaseParamMapper` and `SnakeCaseParamMapper` automatically, we can just as easily use:\n\n```yml\ndatabase:\n  instance-host-name: server1.prd\n```\n\nor\n\n```yml\ndatabase:\n  instance_host_name: server1.prd\n```\n\n\n\n\n## Decoders\n\nHoplite converts the raw value in config files to JDK types using instances of the `Decoder` interface.\nThere are built in decoders for all the standard day to day types, such as primitives, dates, lists, sets, maps, enums, arrow types and so on. The full list is below:\n\n| Basic JDK Types                         | Conversion Notes                                                                                                                                                                                            |\n|-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `String`                                |\n| `Long`                                  |\n| `Int`                                   |\n| `Short`                                 |\n| `Byte`                                  |\n| `Boolean`                               | Creates a Boolean from the following values: `\"true\"`, `\"t\"`, `\"1\"`, `\"yes\"` map to `true` and `\"false\"`, `\"f\"`, `\"0\"`, `\"no\"` map to `false`                                                               |\n| `Double`                                |\n| `Float`                                 |\n| `Enums`                                 | Java and Kotlin enums are both supported. An instance of the defined Enum class will be created with the constant value given in config.                                                                    |\n| `BigDecimal`                            | Converts from a String, Long, Int, Double, or Float into a BigDecimal                                                                                                                                       |\n| `BigInteger`                            | Converts from a String, Long or Int into a BigInteger.                                                                                                                                                      |\n| `UUID`                                  | Creates a `java.util.UUID` from a String                                                                                                                                                                    |\n| `Locale`                                | Creates a `java.util.Locale` from a String                                                                                                                                                                  |\n| **java.time types**                     |                                                                                                                                                                                                             |\n| `LocalDateTime`                         |\n| `LocalDate`                             |\n| `LocalTime`                             |\n| `Duration`                              | Creates a Java `Duration` from a string in a [duration format](#duration-formats) or from a long in milliseconds.                                                                                           |\n| `Instant`                               | Creates an instance of `Instant` from an offset from the unix epoc in milliseconds.                                                                                                                         |\n| `Year`                                  | Creates an instance of `Year` from a String in the format `2007`                                                                                                                                            |\n| `YearMonth`                             | Creates an instance of `YearMonth` from a String in the format `2007-12`                                                                                                                                    |\n| `MonthDay`                              | Creates an instance of `MonthDay` from a String in the format `08-18`                                                                                                                                       |\n| `java.util.Date`                        |                                                                                                                                                                                                             |\n| **Kotlin types**                        |                                                                                                                                                                                                             |\n| `Duration`                              | Creates a kotlin `Duration` from a string in a [duration format](#duration-formats) or from a long in milliseconds.                                                                                         |\n| `ByteArray`                             | Creates a kotlin `ByteArray` from a string.                                                                                                                                                                 |\n| **java.net types**                      |                                                                                                                                                                                                             |\n| `URI`                                   |                                                                                                                                                                                                             |\n| `URL`                                   |                                                                                                                                                                                                             |\n| `InetAddress`                           |                                                                                                                                                                                                             |\n| **JDK IO types**                        |                                                                                                                                                                                                             |\n| `File`                                  | Creates a java.io.File from a String path                                                                                                                                                                   |\n| `Path`                                  | Creates a java.nio.Path from a String path                                                                                                                                                                  |\n| **Kotlin stdlib types**                 |                                                                                                                                                                                                             |\n| `Pair\u003cA,B\u003e`                             | Converts from an array of three two into an instance of `Pair\u003cA,B\u003e`. Will fail if the array does not have exactly two elements.                                                                             |\n| `Triple\u003cA,B,C\u003e`                         | Converts from an array of three elements into an instance of `Triple\u003cA,B,C\u003e`. Will fail if the array does not have exactly three elements.                                                                  |\n| `kotlin.text.Regex`                     | Creates a `kotlin.text.Regex` from a regex compatible string                                                                                                                                                |\n| **Collections**                         |                                                                                                                                                                                                             |\n| `List\u003cA\u003e`                               | Creates a List from either an array or a string delimited by commas.                                                                                                                                        |\n| `Set\u003cA\u003e`                                | Creates a Set from either an array or a string delimited by commas.                                                                                                                                         |\n| `SortedSet\u003cA\u003e`                          | Creates a SortedSet from either an array or a string delimited by commas.                                                                                                                                   |\n| `Map\u003cK,V\u003e`                              |                                                                                                                                                                                                             |\n| `LinkedHashMap\u003cK,V\u003e`                    | A Map that mains the order defined in config                                                                                                                                                                |\n| **Hoplite types**                       |                                                                                                                                                                                                             |\n| `Masked`                                | Wraps a String in a Masked object that redacts toString()                                                                                                                                                   |\n| `SizeInBytes`                           | Returns a SizeInBytes object which parses values like 12MiB or 9KB                                                                                                                                          |\n| `Seconds`                               | Wraps an integer in a `Seconds` object which can be converted to a duration using the `.duration()` extension method.                                                                                       |\n| `Minutes`                               | Wraps an integer in a `Minutes` object which can be converted to a duration using the `.duration()` extension method.                                                                                       |\n| `Base64`                                | Wraps a `ByteBuffer` in a `Base64` object which is only converted if the input is a valid base 64 encoded string.                                                                                           |\n| **javax.security.auth**                 |                                                                                                                                                                                                             |\n| `X500Principal`                         | Creates an instance of `X500Principal` for String values                                                                                                                                                    |\n| `KerberosPrincipal`                     | Creates an instance of `KerberosPrincipal` for String values                                                                                                                                                |\n| `JMXPrincipal`                          | Creates an instance of `JMXPrincipal` for String values                                                                                                                                                     |\n| `Principal`                             | Creates an instance of `BasicPrincipal` for String values                                                                                                                                                   |\n| **Arrow**                               | Requires `hoplite-arrow` module                                                                                                                                                                             |\n| `arrow.data.NonEmptyList\u003cA\u003e`            | Converts arrays into a `NonEmptyList\u003cA\u003e` if the array is non empty. If the array is empty then an error is raised.                                                                                          |\n| `arrow.core.Option\u003cA\u003e`                  | A `None` is used for null or undefined values, and present values are converted to a `Some\u003cA\u003e`.                                                                                                             |\n| `arrow.core.Tuple2\u003cA,B\u003e`                | Converts an array of two elements into an instance of `Tuple2\u003cA,B\u003e`.  Will fail if the array does not have exactly two elements.                                                                            |\n| `arrow.core.Tuple3\u003cA,B,C\u003e`              | Converts an array of three elements into an instance of `Tuple3\u003cA,B,C\u003e`. Will fail if the array does not have exactly three elements.                                                                       |\n| `arrow.core.Tuple4\u003cA,B,C,D\u003e`            | Converts an array of four elements into an instance of `Tuple4\u003cA,B,C,D\u003e`. Will fail if the array does not have exactly four elements.                                                                       |\n| `arrow.core.Tuple5\u003cA,B,C,D,E\u003e`          | Converts an array of five elements into an instance of `Tuple5\u003cA,B,C,D,E\u003e`. Will fail if the array does not have exactly five elements.                                                                     |\n| **Hikari Connection Pool**              | Requires `hoplite-hikaricp` module                                                                                                                                                                          |\n| `HikariDataSource`                      | Converts nested config into a `HikariDataSource`. Any keys nested under the field name will be passed through to the `HikariConfig` object as the datasource is created. Requires `hoplite-hikaricp` module |\n| **Hadoop Types**                        | Requires `hoplite-hdfs` module                                                                                                                                                                              |\n| `org.apache.hadoop.fs.Path`             | Returns instances of HDFS Path objects                                                                                                                                                                      |\n| **CronUtils types**                     | Requires `hoplite-cronutils` module                                                                                                                                                                         |\n| `com.cronutils.model.Cron`              | Returns parsed instance of a cron expression                                                                                                                                                                |\n| **kotlinx datetime Types**              | Requires `hoplite-datetime` module                                                                                                                                                                          |\n| `kotlinx.datetime.LocalDateTime`        |                                                                                                                                                                                                             |\n| `kotlinx.datetime.LocalDate`            |                                                                                                                                                                                                             |\n| `kotlinx.datetime.Instant`              |                                                                                                                                                                                                             |\n| **AWS SDK types**                       | Requires `hoplite-aws` module                                                                                                                                                                               |\n| `com.amazonaws.regions.Region`          |                                                                                                                                                                                                             |\n| **Micrometer types**                    | Requires `hoplite-micrometer-xxx` modules                                                                                                                                                                   |\n| `io.micrometer.statsd.DatadogConfig`    | Converts a nested object to an instance of DatadogConfig                                                                                                                                                    |\n| `io.micrometer.statsd.PrometheusConfig` | Converts a nested object to an instance of PrometheusConfig                                                                                                                                                 |\n| `io.micrometer.statsd.StatsdConfig`     | Converts a nested object to an instance of StatsdConfig                                                                                                                                                     |\n\n\n\n\n\n\n## Duration formats\n\nDuration types support unit strings in the following format (lower case only), with an optional space between the unit value and the unit type.\n\n* `ns`, `nano`, `nanos`, `nanosecond`, `nanoseconds`\n* `us`, `micro`, `micros`, `microsecond`, `microseconds`\n* `ms`, `milli`, `millis`, `millisecond`, `milliseconds`\n* `s`, `second`, `seconds`\n* `m`, `minute`, `minutes`\n* `h`, `hour`, `hours`\n* `d`, `day`, `days`\n\nFor example, `10s`, `3 days`, or `12 hours`.\n\n\n\n\n\n\n\n\n\n## Preprocessors\n\nHoplite supports what it calls preprocessors. These are just functions that are applied to every value as they are read from the underlying config file.\nThe preprocessor is able to transform the value (or return the input - aka identity function) depending on the logic of that preprocessor.\n\nFor example, a preprocessor may choose to perform environment variable substitution, configure default values,\nperform database lookups, or whatever other custom action you need when the config is being resolved.\n\nYou can add custom pre-processors in addition to the built in ones, by using the function `withPreprocessor` on the `ConfigLoader` class, and passing in an instance of the `Preprocessor` interface.\nA typical use case of a custom preprocessor is to lookup some values in a database, or from a third party secrets store such as [Vault](https://www.vaultproject.io/) or [Amazon Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html).\n\nOne way this can be implemented is to have a prefix, and then use a preprocessor to look for the prefix in strings, and if the prefix is present, use the rest of the string as a key to the service. The `PrefixProcessor` abstract class implements this by handling the node traversal, while leaving the specific processing as an exercise for the reader.\n\nFor example\n\n```yaml\ndatabase:\n  user: root\n  password: vault:/my/key/path\n```\n\nNote: You can repeatedly apply preprocessors by setting the property `withPreprocessingIterations` on the `ConfigLoaderBuilder` to a value greater than 1.\nThis causes looped application of all preprocessors. This can be useful if you wish to have one preprocessor resolve a value that then needs to be resolved by another preprocessor.\n\n\n\n\n### Built-in Preprocessors\n\nThese built-in preprocessors are registered automatically.\n\n| Preprocessor                   | Function                                                                                                                                                                                                                                                                                                                                                                                                                                |\n|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `EnvVarPreprocessor`           | Replaces any strings of the form ${VAR} with the environment variable $VAR if defined. These replacement strings can occur between other strings.\u003cbr/\u003e\u003cbr/\u003eFor example `foo: hello ${USERNAME}` would result in foo being assigned the value `hello Sam` assuming the env var `USERNAME` was set to `SAM`. Also the expressions can have default values using the usual bash expression style syntax `foo: hello ${USERNAME:-fallback}` |\n| `SystemPropertyPreprocessor`   | Replaces any strings of the form ${VAR} with the system property $VAR if defined. These replacement strings can occur between other strings.\u003cbr/\u003e\u003cbr/\u003eFor example `debug: ${DEBUG}` would result in debug being assigned the value `true` assuming the application had been started with `-Ddebug=true`                                                                                                                                 |\n| `RandomPreprocessor`           | Inserts random strings into the config. See the section on Random Preprocessor for syntax.                                                                                                                                                                                                                                                                                                                                              |\n| `PropsFilePreprocessor`        | Replaces any strings of the form ${key} with the value of the key in a provided `java.util.Properties` file. The file can be specified by a `Path` or a resource on the classpath.                                                                                                                                                                                                                                                      |\n| `LookupPreprocessor`           | Replaces any strings of the form {{key}} with the value of that node in the already parsed config. In other words, this allow substitution from config in one place to another place (even across files).                                                                                                                                                                                                                               |\n\n\n\n\n### Optional Preprocessors\n\nThese preprocessors must be added to the `ConfigBuilder` before they take effect, and require extra modules to be added to the build.\n\n| Preprocessor                    | Function                                                                                                                                                                                                                                                                                                               |\n|:--------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `AwsSecretsManagerPreprocessor` | Replaces strings of the form awssm://key by looking up the value of 'key' from [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/).\u003cbr/\u003e\u003cbr/\u003eThis preprocessor requires the `hoplite-aws` module to be added to the classpath.                                                                              |\n| `AzureKeyVaultPreprocessor`     | Replaces strings of the form azurekeyvault://key by looking up the value of 'key' from [Azure Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/).\u003cbr/\u003e\u003cbr/\u003eThis preprocessor requires the `hoplite-azure` module to be added to the classpath.                                                              |\n| `ParameterStorePreprocessor`    | Replaces strings of the form ${ssm:key} by looking up the value of 'key' from the [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html).\u003cbr/\u003e\u003cbr/\u003eThis preprocessor requires the `hoplite-aws` module to be added to the classpath. |\n| `ConsulConfigPreprocessor`      | Replaces strings of the form consul://key by looking up the value of 'key' from a [Consul](https://www.consul.io/) server.\u003cbr/\u003e\u003cbr/\u003eThis preprocessor requires the `hoplite-consul` module to be added to the classpath.                                                                                               |\n| `VaultSecretPreprocessor`       | Replaces strings of the form vault://key by looking up the value of 'key' from a [Vault](https://www.vaultproject.io/) instance.\u003cbr/\u003e\u003cbr/\u003eThis preprocessor requires the `hoplite-vault` module to be added to the classpath.                                                                                          |\n| `GcpSecretManagerPreprocessor`  | Replaces strings of the form `gcpsm://projects/{projectId}/secrets/{secretName}/versions/{version:latest}` by looking up the value from a [Google Cloud Secret Manager](https://cloud.google.com/secret-manager) instance.\u003cbr/\u003e\u003cbr/\u003eThis preprocessor requires the `hoplite-gcp` module to be added to the classpath.  |\n\n\n\n\n### Random Preprocessor\n\nThe random preprocessor replaces placeholder strings with random values.\n\n| Placeholder         | Generated random value                   |\n|:--------------------|:-----------------------------------------|\n| ${random.int}       | A random int                             |\n| ${random.int(k)}    | A positive random int between 0 and k    |\n| ${random.int(k, j)} | A random int between k and j             |\n| ${random.double}    | A random double                          |\n| ${random.boolean    | A random boolean                         |\n| ${random.string(k)} | A random alphanumeric string of length k |\n| ${random.uuid}      | A randomly generated type 4 UUID         |\n\nFor example:\n\n```\nmy.number=${random.int}\nmy.bignumber=${random.long}\nmy.uuid=${random.uuid}\nmy.number.less.than.ten=${random.int(10)}\nmy.number.in.range=${random.int[1024,65536]}\n```\n\n## Masked values\n\nIt is quite common to output the resolved config at startup for reference when debugging. In this case, the default `toString` generated by Kotlin's data classes is very useful.\nHowever configuration typically includes sensitive information such as passwords or keys which normally you would not want to appear in logs.\n\nTo avoid sensitive fields appearing in the log output, Hoplite provides a built in type called `Masked` which is a wrapper around a String.\nBy declaring a field to have this type, the value will still be loaded from configuration files, but will not be included in the generated `toString`.\n\nFor example, you may define a config class like this:\n\n`data class Database(val host: String, val user: String, val password: Masked)`\n\nAnd corresponding json config:\n\n```json\n{\n  \"host\": \"localhost\",\n  \"user\": \"root\",\n  \"password\": \"letmein\"\n}\n```\n\nAnd then the output of the Database config class via `toString` would be `Database(host=localhost, user=root, password=****)`\n\nNote: The masking effect only happens if you use `toString`.\nIf you marshall your config to a String using a reflection based tool like Jackson, it will still be able to see the underlying value.\nIn these cases, you would need to register a custom serializer.\nFor the Jackson project, a `HopliteModule` object is available in the `hoplite-json` module.\nRegister this with your Jackson mapper, like `mapper.registerModule(HopliteModule)` and then `Masked` values will be ouputted into Json as \"****\"\n\n\n\n\n\n\n## Inline Classes\n\nSome developers, this writer included, like to have strong types wrapping simple values. For example, a `Port` object rather than an Int.\nThis helps to alleviate Stringy typed development.\nKotlin has support for what it calls inline classes which fulfil this need.\n\nHoplite directly supports inline classes.\nWhen using inline classes, you don't need to nest config keys.\n\nFor example, given the following config classes:\n\n```kotlin\ninline class Port(val value: Int)\ninline class Hostname(val value: String)\ndata class Database(val port: Port, val host: Hostname)\n```\n\nAnd then this config file:\n\n```yaml\nport: 9200\nhost: localhost\n```\n\nWe can parse directly:\n\n```kotlin\nval config = ConfigLoader().loadConfigOrThrow\u003cDatabase\u003e(\"config.file\")\nprintln(config.port) // Port(9200)\nprintln(config.host) // Hostname(\"localhost\")\n```\n\n\n## Sealed Classes\n\nHoplite will support sealed classes where it is able to match up the available config keys with the parameters\nof one of the implementations. For example, lets create a config hierarchy as implementations of a sealed class.\n\n```kotlin\nsealed class Database {\n  data class Elasticsearch(val host: String, val port: Int, val index: String) : Database()\n  data class Postgres(val host: String, val port: Int, val schema: String, val table: String) : Database()\n}\n\ndata class TestConfig(val databases: List\u003cDatabase\u003e)\n```\n\nFor the above definition, if hoplite encountered a `host`, `port`, and `index` then it would be clear\nthat it should instantiate an `Elasticsearch` instance. Similarly, if the config keys were `host`, `port`,\n`schema`, and `table`, then the `Postgres` implementation should be used. If the keys don't match an implementation,\nthe config loader would fail. If keys match multiple implementations then the first match is taken.\n\nFor example, the following yaml config file could be used:\n\n```yaml\ndatabases:\n  - host: localhost\n    port: 9200\n    index: foo\n  - host: localhost\n    port: 9300\n    index: bar\n  - host: localhost\n    port: 5234\n    schema: public\n    table: faz\n```\n\nAnd the output would be:\n\n```\nTestConfig(\n  databases=[\n    Elasticsearch(host=localhost, port=9200, index=foo),\n    Elasticsearch(host=localhost, port=9300, index=bar),\n    Postgres(host=localhost, port=5234, schema=public, table=faz)\n  ]\n)\n```\n\n### Objects in sealed classes\n\nHoplite additionally supports using objects in sealed classes.\nFor example, lets expand the database definition to include an `Embedded` object subclass:\n\n```kotlin\nsealed class Database {\n  data class Elasticsearch(val host: String, val port: Int, val index: String) : Database()\n  data class Postgres(val host: String, val port: Int, val schema: String, val table: String) : Database()\n  object Embedded : Database()\n}\n\ndata class TestConfig(val databases: List\u003cDatabase\u003e)\n```\n\nWe can indicate to Hoplite to use the `Embedded` option in two ways. The first is by referencing the type name:\n\nFor example, in yaml:\n\n```yaml\ndatabase: Embedded\n```\n\nOr in Json:\n\n```json\n{\n  \"database\": \"Embedded\"\n}\n```\n\nThis also works for lists, and we can mix and match:\n\nYaml:\n\n```yaml\ndatabases:\n  - \"Embedded\"\n  - host: localhost\n    port: 9300\n    index: bar\n```\n\nJson:\n\n```json\n{\n  \"databases\": [\"Embedded\", { \"host\": \"localhost\", \"port\": 9200, \"index\": \"foo\" }]\n}\n```\n\nThe second method is only for Json by specifying an empty object:\n\n```json\n{\n  \"database\": { }\n}\n```\n\nWhen using the second option, there must be only a single object instance in the hierarchy, otherwise a disambiguation error is thrown.\nIf you want to support multiple object instances, then refer to the type by name.\n\n\n\n\n\n\n## Reloadable Config\n\nHoplite embraces immutable config, but if you require that config is dynamic, then Hoplite provides a `ReloadableConfig` wrapper.\nThis functionality is available by adding the module `hoplite-watch` to your build. The reloader requires a `ConfigLoader`\nand then accepts one or more `Watchable`s which cause the config to be reloaded when whatever they are watching is triggered.\n\nTo be clear, once Hoplite has parsed a config object, it won't mutate that object. This reloadable wrapper will, in the background,\nreload the config once a watcher is triggered. Then you can obtain the latest parsed config whenever you wish by using the method `getLatest`.\n\nIf you wish to be notified whenever the config is reloaded, you can call `subscribe` on the reloader.\n\nA simple example would be to referesh config every 10 seconds:\n\n```kotlin\n// create a watchable that will trigger every 10 seconds\nval watcher = FixedIntervalWatchable(10.seconds)\n\n// create our config loader which will parse config when invoked\nval loader = ConfigLoaderBuilder.default()\n  .addSource(PropertySource.resource(\"/application.yml\"))\n  .build()\n\n// create the reloader, adding the watcher, the config loader, and specifying the target config class\nval reloader = ReloadableConfig(configLoader, TestConfig::class)\n  .addWatcher(watcher)\n\n// obtain the latest config whenever we want\nreloader.getLatest()\n\n// or subscribe for notifications: (TestConfig) -\u003e Unit\nreloader.subscribe { println(\"New config!: $it\") }\n```\n\nYou can implement the `Watchable` interface directly, with whatever triggering logic you wish, or use one of the\npredefined implementations:\n\n| Watchable              | Function                                                                                                                 |\n|:-----------------------|:-------------------------------------------------------------------------------------------------------------------------|\n| FixedIntervalWatchable | Triggers a reload at a fixed interval specified in millis.                                                               |\n| FileWatcher            | Triggers a reload whenever a file inside a given directory is modified.                                                  |\n| ConsulWatcher          | Triggers whenever a key is added, removed or updated in a `Consul` instance. Requires the `hoplite-watch-consul` module. |\n\n\n\n\n\n## Add on Modules\n\nHoplite makes available several other modules that add functionality outside of the main core module. They are in\nseperate modules because they bring in dependencies from those projects and so the modules are optional.\n\n| Module                        | Function                                                                                                |\n|:------------------------------|:--------------------------------------------------------------------------------------------------------|\n| hoplite-arrow                 | Provides decoders for common arrow types                                                                |\n| hoplite-aws                   | Provides decoders for aws `Region` type and a preprocessor for AWS Secrets Manager and Parameter Store. |\n| hoplite-aws2                  | Provides decoders for aws `Region` type using the AWS v2 SDK.                                           |\n| hoplite-azure                 | Provides a preprocessor for retreiving values from Azure Key Vault.                                     |\n| hoplite-consul                | Provides a preprocessor for retreiving values from a Consul instance.                                   |\n| hoplite-datetime              | Provides decoders for [kotlinx datetime](https://github.com/Kotlin/kotlinx-datetime).                   |\n| hoplite-gcp                   | Provides a preprocessor for retreiving values from Google Cloud Platform Secrets Manager.               |\n| hoplite-hdfs                  | Provides decoder for hadoop `Path`                                                                      |\n| hoplite-hikaricp              | Provides decoder for `HikariDataSource`                                                                 |\n| hoplite-micrometer-datadog    | Provides a decoder for Micrometer's `DatadogConfig` registry                                            |\n| hoplite-micrometer-prometheus | Provides a decoder for Micrometer's `PrometheusConfig` registry                                         |\n| hoplite-micrometer-statsd     | Provides a decoder for Micrometer's `StatsdConfig` registry                                             |\n| hoplite-javax                 | Provides decoders for `java.security.Principal` types.                                                  |\n| hoplite-vault                 | Provides a preprocessor for retrieving values from Hashicorp Vault                                      |\n| hoplite-vavr                  | Provides decoders for [vavr](https://github.com/vavr-io/vavr)                                           |\n\n## GraalVM native image\n\nGraalVM native image example can be found inside an [example-native](https://github.com/sksamuel/hoplite/tree/master/example-native) subdirectory.\n\n## License\n```\nThis software is licensed under the Apache 2 license, quoted below.\n\nCopyright 2019 Stephen Samuel\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not\nuse this file except in compliance with the License. You may obtain a copy of\nthe License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\nLicense for the specific language governing permissions and limitations under\nthe License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsksamuel%2Fhoplite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsksamuel%2Fhoplite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsksamuel%2Fhoplite/lists"}