{"id":20480414,"url":"https://github.com/roman/haskell-etc","last_synced_at":"2025-10-24T22:34:13.606Z","repository":{"id":62435920,"uuid":"82947191","full_name":"roman/Haskell-etc","owner":"roman","description":"Declarative configuration spec for Haskell projects","archived":false,"fork":false,"pushed_at":"2019-04-29T23:18:49.000Z","size":530,"stargazers_count":47,"open_issues_count":23,"forks_count":7,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-09-21T11:21:49.860Z","etag":null,"topics":["cli-options","haskell","json-configuration"],"latest_commit_sha":null,"homepage":null,"language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/roman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-02-23T16:30:20.000Z","updated_at":"2024-01-19T05:17:36.000Z","dependencies_parsed_at":"2022-11-01T21:30:34.761Z","dependency_job_id":null,"html_url":"https://github.com/roman/Haskell-etc","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/roman/Haskell-etc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roman%2FHaskell-etc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roman%2FHaskell-etc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roman%2FHaskell-etc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roman%2FHaskell-etc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roman","download_url":"https://codeload.github.com/roman/Haskell-etc/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roman%2FHaskell-etc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280878345,"owners_count":26406641,"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","status":"online","status_checked_at":"2025-10-24T02:00:06.418Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["cli-options","haskell","json-configuration"],"created_at":"2024-11-15T15:49:58.381Z","updated_at":"2025-10-24T22:34:13.569Z","avatar_url":"https://github.com/roman.png","language":"Haskell","readme":"[![Build Status](https://travis-ci.org/roman/Haskell-etc.svg?branch=master)](https://travis-ci.org/roman/Haskell-etc)\n[![CircleCI](https://circleci.com/gh/roman/Haskell-etc.svg?style=svg)](https://circleci.com/gh/roman/Haskell-etc)\n[![Github](https://img.shields.io/github/commits-since/roman/haskell-etc/v0.4.0.3.svg)](https://img.shields.io/github/commits-since/roman/haskell-etc/v0.4.0.3.svg)\n[![Hackage](https://img.shields.io/hackage/v/etc.svg)](https://img.shields.io/hackage/v/etc.svg)\n[![Hackage Dependencies](https://img.shields.io/hackage-deps/v/etc.svg)](https://img.shields.io/hackage/v/etc.svg)\n[![Stackage LTS](http://stackage.org/package/etc/badge/lts)](http://stackage.org/lts/package/etc)\n[![Stackage Nightly](http://stackage.org/package/etc/badge/nightly)](http://stackage.org/nightly/package/etc)\n\n# etc\n\n`etc` gathers configuration values from multiple sources (cli options, OS\nenvironment variables, files) using a declarative spec file that defines where\nthese values are to be found and located in a configuration map.\n\n## Table Of Contents\n\n* [Raison d'etre](#raison-detre)\n* [Defining a spec file](#defining-a-spec-file)\n* [Reading a spec file](#reading-a-spec-file)\n  * [YAML Support](#yaml-support)\n* [Gathering configuration values explicitly](gathering-configuration-values-explicitly)\n  * [Default](#default)\n  * [Configuration Files](#configuration-files)\n    * [Why have more than one configuration file?](why-have-more-than-one-configuration-file)\n  * [Environment Variables](#environment-variables)\n  * [Command Line](#command-line)\n    * [`opt/cli` entries](#optcli-entries)\n    * [`cli` entries](#cli-entries)\n    * [Using Plain resolver](#using-plain-resolver)\n    * [Using Command resolver](#using-command-resolver)\n    * [CLI Support](#cli-support)\n  * [Reading From Pure Sources](#reading-from-pure-sources)\n* [Accessing configuration values](#accessing-configuration-values)\n* [Printing your configuration values](#printing-your-configuration-values)\n* [Report Misspellings on Environment Variables](#report-misspellings-on-environment-variables)\n* [Cabal Flags](#cabal-flags)\n* [Full Example](#full-example)\n\n\n## Raison d'etre\n\n`etc` is a configuration management that:\n\n* Allows to have a versioned spec of all values your application can accept\n\n* Provides documentation about Environment Variables used by your application\n\n* Provides an API for gathering values from multiple sources (files, overwrite\n  files, cli arguments, environment variables) and then composing them into a\n  single configuration map\n\n* Gives a sane precedence over sources of the configuration value sources\n\n* Provides inspection utilities to understand why the configuration is the way\n  it is\n\n* Provides an API that abstracts away the source of configuration values and\n  allows easy casting into record types your application or other libraries\n  understand\n\n## Defining a spec file\n\nYou need to use a spec file to define the structure of your application's\nconfiguration map; also if an entry value on this configuration map can have\nmultiple input sources\n([environment variable](#environment-variables),\n[configuration files](#configuration-files),\n[command line option](#command-line), etc), you can specify right there what\nthis sources may be. The map can be defined using JSON or [YAML](#yaml-support);\nfollowing an example in YAML format:\n\n```yaml\n###\n# These paths are going to be read for configuration values merging values\n# from all of them, if they have the same keys, entries on (2) will have\n# precedence over entries on (1)\netc/filepaths:\n- ./resources/config.json # 1\n- /etc/my-app/config.json # 2\n\n###\n# The program is going to have a Command Line interface\netc/cli:\n  desc: \"Description of the program that reads this configuration spec\"\n  header: \"my-app - A program that has declarative configuration input\"\n\n  # The program is going to have 2 sub-commands\n  commands:\n    config:\n      desc: \"Prints configuration summary\"\n      header: \"\"\n    run:\n      desc: \"Executes main program\"\n      header: \"\"\n\n###\n# With etc/entries we define the configuration map structure your\n# application is going to be reading values from\netc/entries:\n  credentials:\n    username:\n      # Define the spec for [\"credentials\", \"username\"]\n      etc/spec:\n        type: string\n        # default value (least precedence)\n        default: \"root\"\n\n        # if environment variable is defined, put its value in this entry\n        env: \"MY_APP_USERNAME\"\n\n        # cli input is going to have one option for this value\n        cli:\n          input: option\n          metavar: USERNAME\n          help: Username of the system\n          required: false\n          # option is going to be available only on run sub-command\n          commands:\n          - run\n\n    # Define the spec for [\"credentials\", \"password\"]\n    password:\n      etc/spec:\n        type: string\n        env: \"MY_APP_PASSWORD\"\n        cli:\n          input: option\n          metavar: PASSWORD\n          help: \"Password of user\"\n          required: true\n          commands:\n          - run\n```\n\nThe important keys to notice on the previous example:\n\n- `etc/filepaths` tells where to look for files to gather the configuration of\nyour app, it could be more than one file because you may want to have a default\nfile for development, and then override it with some configurations for\nproduction/integration, The further the filepath is the higher precedence its\nvalues are going to have.\n\n- `etc/entries` specifies how your configuration map is going to look like and how\nyour business logic [will be accessing it](#accessing-configuration-values)\n\n- `etc/spec` provide means to define metadata for a configuration value entry,\nwhat is its default value, if it can be found via\nan [environment variable](#environment-variables), or if it may be specified as\nan [CLI option/argument](#command-line) input.\n\n## Reading a spec file\n\nTo read a spec file you need to use the `System.Etc.readConfigSpec` function, this\nfunction can accept either a JSON or YAML filepath. You can also use the\n`System.Etc.parseConfigSpec` if you already gather the contents of a spec file from a\ndifferent source.\n\n_NOTE_: When using `System.Etc.parseConfigSpec` or `System.Etc.readConfigSpec`\nand the [CLI cabal feature flag is true](#cli-support), unless you use the\n`System.Etc.resolveCommandCli` function, you will have to explicitly declare the\n`ConfigSpec` type parameter.\n\n### YAML support\n\nIn order to allow `etc` to read from YAML files, you will need to use the `yaml`\ncabal flag when installing the library, here are some instructions on how to\npass cabal flags\nusing\n[stack](https://docs.haskellstack.org/en/stable/nonstandard_project_init/?highlight=cabal%20flags#passing-flags-to-cabal) and\n[cabal](http://stackoverflow.com/a/26490956/132987). We do this so that in case\nyou want to stick with the JSON format, you don't have to pull dependencies you\ndon't need.\n\n## Gathering configuration values explicitly\n\nEven though a spec file defines where the configuration values can be found,\n`etc` won't collect those values unless it is explicitly told to do so. To do\nthis you must use functions that will _resolve_ these configuration sources.\n\n### Default\n\nWhen defining the spec, you can specify default values on the `etc/spec`\nmetadata entry. To get this values from the spec you must call the\n`System.Etc.resolveDefault` with the result from `System.Etc.readConfigSpec` as an argument.\n\n#### Example\n\n```haskell\nimport qualified System.Etc as Etc\n\ngetConfiguration :: IO Etc.Config\ngetConfiguration = do\n  spec \u003c- Etc.readConfigSpec \"./path/to/spec.yaml\"\n  return (Etc.resolveDefault spec)\n```\n\n### Configuration Files\n\nTo get values from configuration files on your filesystem, you must specify an\n`etc/filepaths` entry on the spec file, this will tell `etc` to merge a list of\nconfiguration values from each path, the latter the filepath, the more\nprecedence it has on the configuration map.\n\nAfter this entry is defined in your spec, you must then call the\n`System.Etc.resolveFiles` function with the result of `System.Etc.readConfigSpec` as a\nparameter.\n\n#### Why have more than one configuration file?\n\nThis helps to have a scheme of over-writable configurations on deployed\napplications, you can have the first path in the list of `etc/filepaths` entry\nbe the config used while developing your app, and once deployed you can have\nproduction configuration values on a well known path (say\n`/etc/my-app/config.yaml`).\n\n#### Example\n\n```haskell\nimport Data.Monoid (mappend)\nimport qualified System.Etc as Etc\n\ngetConfiguration :: IO Etc.Config\ngetConfiguration = do\n  spec \u003c- Etc.readConfigSpec \"./path/to/spec.yaml\"\n\n  let\n    defaultConfig =\n      Etc.resolveDefault spec\n\n  (fileConfig, _fileWarnings) \u003c- Etc.resolveFiles spec\n\n  return (fileConfig `mappend` defaultConfig)\n```\n\n### Environment Variables\n\nWhen an `env` key is specified in the `etc/spec` metadata of a configuration\nvalue entry, `etc` will consider an environment variable with the given name.\n\nAfter this entry is defined in your spec, you must then call the\n`System.Etc.resolveEnv` function with the result of `System.Etc.readConfigSpec` as a\nparameter.\n\n#### Example\n\n```haskell\nimport Data.Monoid (mappend)\nimport qualified System.Etc as Etc\n\ngetConfiguration :: IO Etc.Config\ngetConfiguration = do\n  spec \u003c- Etc.readConfigSpec \"./path/to/spec.yaml\"\n\n  let\n    defaultConfig =\n      Etc.resolveDefault spec\n\n  (fileConfig, _fileWarnings) \u003c- Etc.resolveFiles spec\n  envConfig  \u003c- Etc.resolveEnv spec\n\n  return (fileConfig `mappend` envConfig `mappend` defaultConfig)\n```\n\n### Command Line\n\nYou can setup a CLI input for your program by using the `etc/cli` entry at the\nroot of the spec file, and the `cli` entry on the `etc/spec` metadata entries\nfor configuration values.\n\nWhen a `cli` key is specified in the `etc/spec` metadata of a configuration\nvalue entry, `etc` will consider inputs from a command line interface for your\napplication.\n\n#### `opt/cli` entries\n\nThe `opt/cli` entry map must have the following keys:\n\n- `desc`: A one line description of what your application does\n\n- `header`: The header used when getting the information from the auto-generated\n  `--help` option\n\n- `commands`: A map of sub-commands that this program can have; each entry is\n   the name of the sub-command, and the value is a map with the key `desc` with\n   the same purpose as the top-level `desc` entry defined above.\n\n   *NOTE*: you must use [`System.Etc.resolveCommandCli`](#using-command-resolver) for\n   the `commands` entry to take effect\n\n#### `cli` entries\n\nThe `cli` entry map can have the following keys (`input` is required):\n\n- `required`: specifies if the entry is required on the CLI\n\n- `input`: how you want to receive the input value, it can either be `argument`\n  or `option`\n\n- `metavar`: the name of the input argument on the example/documentation string\n  of the CLI help\n\n- `long` (only available on `option` inputs): the name of the option in long\n  form (e.g. `--name`)\n\n- `short` (only available on `option` inputs): the name of the option in short\n  form (.e.g `-n`)\n\n- `commands`: A list of sub-commands that are going to have this option/argument\n  available; make sure the commands listed here are also listed in the `etc/cli`\n  entry of your spec file.\n\n#### Using Plain resolver\n\nWhen the `commands` key *is not* specified on the `etc/cli` entry of the spec\nfile, you *must* use this resolver.\n\nAfter the `cli` entry is defined in your spec, you must then call the\n`System.Etc.resolvePlainCli` function with the result of `System.Etc.readConfigSpec` as a\nparameter.\n\n##### Example\n\n```haskell\nimport Data.Monoid (mappend)\nimport qualified System.Etc as Etc\n\ngetConfiguration :: IO Etc.Config\ngetConfiguration = do\n  spec \u003c- Etc.readConfigSpec \"./path/to/spec.yaml\"\n\n  let\n    defaultConfig =\n      Etc.resolveDefault spec\n\n  (fileConfig, _fileWarnings) \u003c- Etc.resolveFiles spec\n  envConfig  \u003c- Etc.resolveEnv spec\n  cliConfig  \u003c- Etc.resolvePlainCli spec\n\n  return (fileConfig\n          `mappend` cliConfig\n          `mappend` envConfig\n          `mappend` defaultConfig)\n```\n\n#### Using Command resolver\n\nWhen the `commands` key *is* specified on the `etc/cli` entry of the spec file, you *must*\nuse this resolver.\n\nAfter the `cli` entry is defined in your spec, you must then call the\n`System.Etc.resolveCommandCli` function with the result of `System.Etc.readConfigSpec` as a\nparameter.\n\nThis will return a tuple with the chosen sub-command and the configuration map;\nthe command Haskell type needs to be an instance of the `Aeson.FromJSON`,\n`Aeson.ToJSON` and `Data.Hashable.Hashable` typeclasses for the command to be\nparsed/serialized effectively.\n\n##### Example\n\n```haskell\nimport GHC.Generics (Generic)\nimport Data.Hashable (Hashable)\nimport qualified Data.Aeson as JSON\nimport qualified Data.Aeson.Types as JSON (typeMismatch)\nimport Data.Monoid (mappend)\nimport qualified System.Etc as Etc\n\ndata Cmd\n  = Config\n  | Run\n  deriving (Show, Eq, Generic)\n\ninstance Hashable Cmd\n\ninstance JSON.FromJSON Cmd where\n  parseJSON json =\n    case json of\n      JSON.String cmdName -\u003e\n        if cmdName == \"config\" then\n          return Config\n        else if cmdName == \"run\" then\n          return Run\n        else\n          JSON.typeMismatch (\"Cmd (\" `mappend` Text.unpack cmdName `mappend` \")\") json\n      _ -\u003e\n        JSON.typeMismatch \"Cmd\" json\n\ninstance JSON.ToJSON Cmd where\n  toJSON cmd =\n    case cmd of\n      Config -\u003e\n        JSON.String \"config\"\n      Run -\u003e\n        JSON.String \"run\"\n\ngetConfiguration :: IO (Cmd, Etc.Config)\ngetConfiguration = do\n  spec \u003c- Etc.readConfigSpec \"./path/to/spec.yaml\"\n\n  let\n    defaultConfig =\n      Etc.resolveDefault spec\n\n  envConfig  \u003c- Etc.resolveEnv spec\n  (fileConfig, _fileWarnings) \u003c- Etc.resolveFiles spec\n  (cmd, cliConfig) \u003c- Etc.resolveCommandCli spec\n\n  return ( cmd\n         , fileConfig\n          `mappend` cliConfig\n          `mappend` envConfig\n          `mappend` defaultConfig)\n```\n\n### CLI Support\n\nIn order to allow `etc` to generate CLI inputs for your program, you will need\nto use the `cli` cabal flag when installing the library, here are some\ninstructions on how to pass cabal flags\nusing\n[stack](https://docs.haskellstack.org/en/stable/nonstandard_project_init/?highlight=cabal%20flags#passing-flags-to-cabal) and\n[cabal](http://stackoverflow.com/a/26490956/132987). We do this so that in case\nyou are not interested in generating a CLI input for your program, you don't\nhave to pull dependencies you don't need.\n\n### Reading from pure sources\n\nSometimes, you would like to use the concept of CLI or environment variables, without\nactually calling the OS APIs, `etc` provides pure versions for these resolvers:\n\n- `System.Etc.resolveEnvPure`\n\n- `System.Etc.resolvePlainCliPure`\n\n- `System.Etc.resolveCommandCliPure`\n\nThis work exactly the same as their non-pure counterparts, but receive one extra\nargument to fetch the required input.\n\n## Accessing Configuration Values\n\nInternally, `etc` stores every value that it gathers from all sources like a\nJSON object (using the `Data.Aeson.Value` type), this provides a lot of\nflexibility around what value you can get from your configuration map, allowing\nyour to use Aeson typeclasses to cast configuration values to more business\nlogic data structures.\n\nThere are two functions that can be used to get values out from a configuration\nmap:\n\n- `System.Etc.getConfigValue`\n\nReads values specified on a spec file and casts it to a Haskell type\nusing the `Aeson.FromJSON` typeclass\n\n- `System.Etc.getConfigValueWith`\n\nReads values specified on a spec file and casts it using a custom function that\nuses the `Aeson` parser API; this works great when the data structures of\nlibraries you use don't support `Aeson` or the format in your config file is not\nquite the same as the already implemented `Aeson.FromJSON` parser of a type\ngiven by a library.\n\nAn example of their usage is given in the [full example](#full-example) section\n\n## Printing your configuration values\n\nA lot of times you may want to assert where a configuration value is coming\nfrom, or if a particular environment variable was considered effectively by your\nprogram. You an use the `System.Etc.printPrettyConfig` function to render the\nconfiguration map and the different values/sources that were resolved when\ncalculating it. This function is _really_ useful for debugging purposes.\n\n### Example\n\nHere is the output of one of\nthe\n[example applications](https://github.com/roman/Haskell-etc/tree/master/etc-command-example):\n\n```bash\n$ MY_APP_USERNAME=foo etc-command-example run -u bar -p 123\nExecuting main program\ncredentials.username\n  bar        [ Cli        ]\n  foo        [ Env: MY_APP_USERNAME ]\n  root       [ Default    ]\n\ncredentials.password\n  123        [ Cli        ]\n```\n\nThe output displays all the configuration values and their sources, the first\nvalue on the list is the value that `System.Etc.getConfigValue` returns for that\nparticular key.\n\n## Report Misspellings on Environment Variables\n\nWhen you define `env` keys on the `etc/entries` map of your spec file, we can\ninfer what are the valid Environment Variables that need to be defined for your\napplication, knowing this, `etc` can infer when there is a typo on one of this\nenvironment variables and report this. You need to have the `extra` cabal flag and\ncall the `System.Etc.reportEnvMisspellingWarnings` with the configuration spec as\nas an argument.\n\n### Example\n\nHere is an example of the output this function prints to `stderr` when the given\nEnvironment Variables are almost identical to the ones found on the spec file:\n\n```bash\n$ MY_AP_USERNAME=foo etc-command-example run -u bar -p 123\n\nWARNING: Environment variable `MY_AP_USERNAME' found, perhaps you meant `MY_APP_USERNAME'\n\n```\n\n## Cabal Flags\n\nTo reduce the amount of dependencies this library brings, you can choose\nthe exact bits of functionality you need for your application.\n\n- `yaml`: Allows (in addition of JSON) to have spec file and configuration files\n  in YAML format\n\n- `cli`: Provides the CLI functionality explained in this README\n\n- `extra`: Provides helper functions for inspecting the resolved configuration\n  as well as providing warning messages for misspelled environment variables\n\n## Full Example\n\n*NOTE*: This example uses the [spec file stated above](#defining-a-spec-file)\n\n```haskell\nimport Control.Applicative ((\u003c$\u003e), (\u003c*\u003e))\nimport Data.Aeson ((.:))\nimport Data.Hashable (Hashable)\nimport Data.Monoid (mappend)\nimport GHC.Generics (Generic)\n\nimport qualified Data.Aeson as JSON\nimport qualified Data.Aeson.Types as JSON (typeMismatch)\nimport qualified System.Etc as Etc\n\ndata Credentials\n  = Credentials { username :: Text\n                , password :: Text }\n  deriving (Show)\n\ndata Cmd\n  = Config\n  | Run\n  deriving (Show, Eq, Generic)\n\ninstance Hashable Cmd\n\ninstance JSON.FromJSON Cmd where\n  parseJSON json =\n    case json of\n      JSON.String cmdName -\u003e\n        if cmdName == \"config\" then\n          return Config\n        else if cmdName == \"run\" then\n          return Run\n        else\n          JSON.typeMismatch (\"Cmd (\" `mappend` Text.unpack cmdName `mappend` \")\") json\n      _ -\u003e\n        JSON.typeMismatch \"Cmd\" json\n\ninstance JSON.ToJSON Cmd where\n  toJSON cmd =\n    case cmd of\n      Config -\u003e\n        JSON.String \"config\"\n      Run -\u003e\n        JSON.String \"run\"\n\nparseCredentials json =\n  case json of\n    JSON.Object object -\u003e\n      Credentials\n        \u003c$\u003e object .: \"user\"\n        \u003c*\u003e object .: \"password\"\n\ngetConfiguration :: IO (Cmd, Etc.Config)\ngetConfiguration = do\n  spec \u003c- Etc.readConfigSpec \"./path/to/spec.yaml\"\n\n  Etc.reportEnvMisspellingWarnings spec\n\n  let\n    defaultConfig =\n      Etc.resolveDefault spec\n\n  (fileConfig, _fileWarnings) \u003c- Etc.resolveFiles spec\n  envConfig  \u003c- Etc.resolveEnv spec\n  (cmd, cliConfig) \u003c- Etc.resolveCommandCli spec\n\n  return ( cmd\n         , fileConfig\n          `mappend` cliConfig\n          `mappend` envConfig\n          `mappend` defaultConfig )\n\nmain :: IO ()\nmain = do\n  (cmd, config) \u003c- getConfiguration\n\n  case cmd of\n    Config -\u003e do\n      Etc.printPrettyConfig config\n\n    Run -\u003e do\n      -- Get individual entries (Uses instance of Text type for the Aeson.FromJSON\n      -- typeclass)\n      username \u003c- Etc.getConfigValue [\"credentials\", \"username\"]\n\n      -- Get the values with a supplied JSON parser\n      creds \u003c- Etc.getConfigValueWith parseCredentials [\"credentials\"]\n\n      print (username :: Text)\n      print creds\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froman%2Fhaskell-etc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froman%2Fhaskell-etc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froman%2Fhaskell-etc/lists"}