{"id":13482785,"url":"https://github.com/icyleaf/totem","last_synced_at":"2025-04-16T03:43:52.549Z","repository":{"id":53485427,"uuid":"140830733","full_name":"icyleaf/totem","owner":"icyleaf","description":"Crystal configuration with spirit. Load and parse configuration in JSON, YAML, dotenv formats.","archived":false,"fork":false,"pushed_at":"2024-02-23T02:59:27.000Z","size":1224,"stargazers_count":69,"open_issues_count":1,"forks_count":5,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-29T04:42:56.256Z","etag":null,"topics":["configuration","crystal"],"latest_commit_sha":null,"homepage":"","language":"Crystal","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/icyleaf.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}},"created_at":"2018-07-13T10:01:02.000Z","updated_at":"2025-01-15T17:08:55.000Z","dependencies_parsed_at":"2024-05-02T19:54:15.868Z","dependency_job_id":"e981f060-b9bd-4da5-8d75-d0e5af0d23b6","html_url":"https://github.com/icyleaf/totem","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Ftotem","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Ftotem/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Ftotem/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyleaf%2Ftotem/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/icyleaf","download_url":"https://codeload.github.com/icyleaf/totem/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249191903,"owners_count":21227674,"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":["configuration","crystal"],"created_at":"2024-07-31T17:01:05.493Z","updated_at":"2025-04-16T03:43:52.495Z","avatar_url":"https://github.com/icyleaf.png","language":"Crystal","funding_links":[],"categories":["Configuration"],"sub_categories":[],"readme":"![totem-logo](https://github.com/icyleaf/totem/raw/master/logo-small.png)\n\n# Totem\n\n[![Language](https://img.shields.io/badge/language-crystal-776791.svg)](https://github.com/crystal-lang/crystal)\n[![Tag](https://img.shields.io/github/tag/icyleaf/totem.svg)](https://github.com/icyleaf/totem/blob/master/CHANGELOG.md)\n[![Build Status](https://img.shields.io/circleci/project/github/icyleaf/totem/master.svg?style=flat)](https://circleci.com/gh/icyleaf/totem)\n\nCrystal configuration with spirit. Inspired from Go's [viper](https://github.com/spf13/viper). Totem Icon by lastspark from [Noun Project](https://thenounproject.com).\n\nConfiguration file formats is always the problem, you want to focus on building awesome things. Totem is here to help with that.\n\nTotem has following features:\n\n- Reading from JSON, YAML, dotenv formats config files or raw string.\n- Reading from environment variables.\n- Reading from remote key-value store systems(redis/etcd).\n- Provide a mechanism to set default values for your different configuration options.\n- Provide an alias system to easily rename parameters without breaking existing code.\n- Write configuration to file with JSON, YAML formats.\n- Convert config to struct with builder.\n\nAnd we keep it minimize and require what you want with adapter and remote provider! **No more dependenices what you do not need**.\nOnly JSON and YAML adapters were auto requires.\n\nUses the following precedence order. Each item takes precedence over the item below it:\n\n- alias\n- override, explicit call to `set`\n- env\n- config\n- kvstores\n- default\n\nTotem configuration keys are case insensitive.\n\n\u003c!-- TOC --\u003e\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n  - [Operating configuration](#operating-configuration)\n  - [Loading configuration](#loading-configuration)\n    - [From raw string](#from-raw-string)\n    - [From file](#from-file)\n- [Usage](#usage)\n  - [Load configuration with multiple paths](#load-configuration-with-multiple-paths)\n  - [Set Alias and using alias](#set-alias-and-using-alias)\n  - [Working with nested key](#working-with-nested-key)\n  - [Working with environment variables](#working-with-environment-variables)\n  - [Working with remote providers](#working-with-remote-providers)\n    - [Use redis](#use-redis)\n    - [Use etcd](#use-etcd)\n  - [Iterating configuration](#iterating-configuration)\n  - [Serialization](#serialization)\n  - [Storing configuration to file](#storing-configuration-to-file)\n- [Advanced Usage](#advanced-usage)\n  - [Use config builder](#use-config-builder)\n  - [Write a config adapter](#write-a-config-adapter)\n  - [Write a remote provider](#write-a-remote-provider)\n- [Q \u0026 A](#q--a)\n  - [How to debug?](#how-to-debug)\n- [Help and Discussion](#help-and-discussion)\n- [Donate](#donate)\n- [How to Contribute](#how-to-contribute)\n- [You may also like](#you-may-also-like)\n- [License](#license)\n\n\u003c!-- /TOC --\u003e\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  totem:\n    github: icyleaf/totem\n```\n\n## Quick Start\n\n```crystal\nrequire \"totem\"\n```\n\n### Operating configuration\n\n```crystal\ntotem = Totem.new\ntotem.set_default(\"name\", \"foo\")\ntotem.set_defaults({\n  \"age\"    =\u003e 18,\n  \"gender\" =\u003e \"male\",\n  \"hobbies\" =\u003e [\n    \"skateboarding\",\n    \"snowboarding\",\n    \"go\"\n  ]\n})\n\ntotem.get(\"name\").as_s # =\u003e \"foo\"\ntotem.get(\"age\").as_i # =\u003e 18\ntotem.set(\"name\", \"bar\")\ntotem.alias(alias_key: \"key\", key: \"name\")\ntotem.get(\"name\").as_s # =\u003e \"bar\"\ntotem.get(\"key\").as_s # =\u003e \"bar\"\n```\n\n### Loading configuration\n\nSupport `JSON`, `YAML` and dotenv data from raw string and file.\n\n#### From raw string\n\nLoad yaml string\n\n```crystal\nraw = \u003c\u003c-EOF\nHacker: true\nname: steve\nhobbies:\n- skateboarding\n- snowboarding\n- go\nclothing:\n  jacket: leather\n  trousers: denim\n  pants:\n    size: large\nage: 35\neyes : brown\nEOF\n\ntotem = Totem.from_yaml raw\ntotem.get(\"Hacker\").as_bool                           # =\u003e true\ntotem.get(\"age\").as_i                                 # =\u003e 35\ntotem.get(\"clothing\").as_h[\"pants\"].as_h[\"size\"].as_s # =\u003e \"large\"\n```\n\nLoad json string\n\n```crystal\nraw = \u003c\u003c-EOF\n{\n  \"id\": \"0001\",\n  \"type\": \"donut\",\n  \"name\": \"Cake\",\n  \"ppu\": 0.55,\n  \"batters\": {\n    \"batter\": [\n      {\n        \"type\": \"Regular\"\n      },\n      {\n        \"type\": \"Chocolate\"\n      },\n      {\n        \"type\": \"Blueberry\"\n      },\n      {\n        \"type\": \"Devil's Food\"\n      }\n    ]\n  }\n}\nEOF\n\ntotem = Totem.from_json raw\ntotem.get(\"name\")                                         # =\u003e \"Cake\"\ntotem.get(\"ppu\")                                          # =\u003e 0.55\ntotem.get(\"batters\").as_h[\"batter\"].as_a[0].as_h[\"type\"]  # =\u003e \"Regular\"\n```\n\nLoad dotenv string\n\n\u003e Add [poncho](https://github.com/icyleaf/poncho) to `shards.yml` and require the adapter.\n\n```crystal\nrequire \"totem\"\nrequire \"totem/config_types/env\"    # Make sure you require\n\nraw = \u003c\u003c-EOF\n# COMMENTS=work\nSTR='foo'\nSTR_WITH_COMMENTS=bar         # str with comment\nSTR_WITH_HASH_SYMBOL=\"abc#123\"#stick comment\nINT=33\nEOF\n\ntotem = Totem.from_env raw\ntotem.get(\"str\")                    # =\u003e \"foo\"\ntotem.get(\"str_with_comments\")      # =\u003e bar\ntotem.get(\"str_with_hash_symbol\")   # =\u003e \"abc#123\"\ntotem.get(\"int\")                    # =\u003e \"33\"\n```\n\n#### From file\n\n\u003e Add [poncho](https://github.com/icyleaf/poncho) to `shards.yml` and require the adapter if you need load dotenv file.\n\n```crystal\n# Load yaml file from file with path\ntotem = Totem.from_file \"./spec/fixtures/config.yaml\"\n\n# Load json file from file with multi-paths\ntotem = Totem.from_file \"config.yaml\", [\"/etc\", \".\", \"./spec/fixtures\"]\n\n# Load dotenv file\ntotem = Totem.from_file \"config.env\"\n```\n\n## Usage\n\n### Load configuration with multiple paths\n\nTotem can search multiple paths, but currently a single Totem instance only supports a single\nconfiguration file.\n\n```crystal\ntotem = Totem.new(\"config\", \"/etc/totem/\")  # =\u003e New a instance with name and path of config file\ntotem.config_paths \u003c\u003c \"~/.totem\"            # =\u003e path to look for the config file in\ntotem.config_paths \u003c\u003c \"./config\"            # =\u003e optionally look for config in the working directory\nbegin\n  totem.load!                               # =\u003e Find and read the config file (order by yaml/yml/json/env)\nrescue e\n  puts \"Fatal error config file: #{e.message}\"\nend\n```\n\n### Set Alias and using alias\n\nAliases permit a single value to be referenced by multiple keys\n\n```crystal\ntotem.alias(\"nickname\", \"Name\")\n\ntotem.set(\"name\", \"foo\")\ntotem.set(\"nickname\", \"bar\")\n\ntotem.get(\"name\")       # =\u003e \"foo\"\ntotem.get(\"nickname\")   # =\u003e \"foo\"\n```\n\n### Working with nested key\n\nAll accessor methods accept nested key:\n\n```crystal\ntotem.set_default(\"profile.user.name\", \"foo\")\ntotem.set(\"profile.user.age\", 13)\ntotem.alias(\"username\", \"profile.user.name\")\ntotem.bind_env(\"profile.user.nickname\", \"PROFILE_USER_NICKNAME\")\ntotem.get(\"profile.user.age\")\n```\n\n### Working with environment variables\n\nTotem has full support for environment variables, example:\n\n```crystal\nENV[\"ID\"] = \"123\"\nENV[\"FOOD\"] = \"Pinapple\"\nENV[\"NAME\"] = \"Polly\"\n\ntotem = Totem.new\n\ntotem.bind_env(\"ID\")\ntotem.get(\"id\").as_i        # =\u003e 123\n\ntotem.bind_env(\"f\", \"FOOD\")\ntotem.get(\"f\").as_s         # =\u003e \"Pinapple\"\n\ntotem.automatic_env\ntotem.get(\"name\").as_s      # =\u003e \"Polly\"\n```\n\nWorking with environment prefix:\n\n```crystal\ntotem.automatic_env(prefix: \"totem\")\n# Same as\n# totem.env_prefix = \"totem\"\n# totem.automatic_env = true\n\ntotem.get(\"id\").as_i    # =\u003e 123\ntotem.get(\"food\").as_s  # =\u003e \"Pinapple\"\ntotem.get(\"name\").as_s  # =\u003e \"Polly\"\n```\n\n### Working with remote providers\n\nTotem retrieve configuration from Key-Value store, which means that you can get your configuration values on the air.\nAvaliable providers is `redis` and `etcd`.\n\n#### Use redis\n\nIt dependency [crystal-redis](https://github.com/stefanwille/crystal-redis) shard. Install it before use.\n\n```crystal\nrequire \"totem\"\nrequire \"totem/remote_providers/redis\"\n\ntotem = Totem.new\ntotem.add_remote(provider: \"redis\", endpoint: \"redis://localhost:6379/0\")\n\ntotem.get(\"user:name\")      # =\u003e \"foo\"\ntotem.get(\"user:id\").as_i   # =\u003e 123\n```\n\nYou can also get raw data from one key with `path`:\n\n```crystal\ntotem.config_type = \"json\"  # There is no file extension in a stream data, supported extensions are all registed config types in Totem.\ntotem.add_remote(provider: \"redis\", endpoint: \"redis://localhost:6379/0\", path: \"config:development\")\n\ntotem.get(\"user:name\")      # =\u003e \"foo\"\ntotem.get(\"user:id\").as_i   # =\u003e 123\n```\n\n#### Use etcd\n\nIt dependency [etcd-crystal](https://github.com/icyleaf/etcd-crystal) shard and ONLY works etcd `v2` API. Install it before use.\n\n```crystal\nrequire \"totem\"\nrequire \"totem/remote_providers/etcd\"\n\ntotem = Totem.new\ntotem.add_remote(provider: \"etcd\", endpoint: \"http://localhost:2379\")\n\ntotem.get(\"user:name\")      # =\u003e \"foo\"\ntotem.get(\"user:id\").as_i   # =\u003e 123\n```\n\nYou can also get raw data from one key with `path`:\n\n```crystal\ntotem.config_type = \"yaml\"  # There is no file extension in a stream data, supported extensions are all registed config types in Totem.\ntotem.add_remote(provider: \"etcd\", endpoint: \"http://localhost:2379\", path: \"/config/development.yaml\")\n\ntotem.get(\"user:name\")      # =\u003e \"foo\"\ntotem.get(\"user:id\").as_i   # =\u003e 123\n```\n\n### Iterating configuration\n\nIterate in Totem is very easy, you can get `#keys`, `#flat_keys`, `#settings` (a.k.a `#to_h`) even iterating it directly with `#each`:\n\n```crystal\ntotem.settings    # =\u003e {\"id\" =\u003e 123, \"user\" =\u003e {\"name\" =\u003e \"foobar\", \"age\" =\u003e 20}}\ntotem.keys        # =\u003e [\"id\", \"user\"]\ntotem.flat_keys   # =\u003e [\"id\", \"user.name\", \"user.age\"]\n\ntotem.each do |key, value|\n  # do something\nend\n```\n\n### Serialization\n\nSerialize configuration to `Struct`, at current stage you can pass a `JSON::Serializable`/`YAML::Serializable` struct to mapping.\n\n```crystal\nstruct Profile\n  include JSON::Serializable\n\n  property name : String\n  property hobbies : Array(String)\n  property age : Int32\n  property eyes : String\nend\n\ntotem = Totem.from_file \"spec/fixtures/config.yaml\"\nprofile = totem.mapping(Profile)\nprofile.name      # =\u003e \"steve\"\nprofile.age       # =\u003e 35\nprofile.eyes      # =\u003e \"brown\"\nprofile.hobbies   # =\u003e [\"skateboarding\", \"snowboarding\", \"go\"]\n```\n\nSerialize configuration with part of key:\n\n```crystal\nstruct Clothes\n  include JSON::Serializable\n\n  property jacket : String\n  property trousers : String\n  property pants : Hash(String, String)\nend\n\ntotem = Totem.from_file \"spec/fixtures/config.yaml\"\nclothes = profile.mapping(Clothes, \"clothing\")\n# =\u003e Clothes(@jacket=\"leather\", @pants={\"size\" =\u003e \"large\"}, @trousers=\"denim\")\n```\n\n### Storing configuration to file\n\nSimple to use `#store!` method.\n\n```crystal\nraw = \u003c\u003c-EOF\nHacker: true\nname: steve\nhobbies:\n- skateboarding\n- snowboarding\n- go\nclothing:\n  jacket: leather\n  trousers: denim\n  pants:\n    size: large\nage: 35\neyes : brown\nEOF\n\ntotem = Totem.from_yaml raw\ntotem.set(\"nickname\", \"Freda\")\ntotem.set(\"eyes\", \"blue\")\ntotem.store!(\"profile.json\")\n```\n\n## Advanced Usage\n\n### Use config builder\n\nYou can generate a configuration with Totem builder with any **Object**.\n\n```crystal\nstruct Configuration\n  include Totem::ConfigBuilder\n\n  build do\n    config_type \"json\"\n    config_paths [\"/etc/totem\", \"~/.config/totem\", \"config/\"]\n  end\nend\n\nconfig = Configuration.configure do |c|\n  c.set_default \"name\", \"foobar\"\nend\n\nconfig[\"name\"] # =\u003e \"foobar\"\n```\n\nThe builder also could mapping config to struct.\n\n```crystal\nstruct Profile\n  include Totem::ConfigBuilder\n\n  property name : String\n  property hobbies : Array(String)\n  property age : Int32\n  property eyes : String\n\n  build do\n    config_type \"yaml\"\n    config_paths [\"/etc/totem\", \"~/.config/totem\", \"config/\"]\n  end\nend\n\nprofile = Profile.configure\nprofile.name          # =\u003e \"steve\"\nprofile[\"nested.key\"] # =\u003e \"foo\"\n```\n\n### Write a config adapter\n\nCreating the custom adapter by integration `Totem::ConfigTypes::Adapter` abstract class. Here has two methods must be implement:\n`read` and `write`. For example, let us write a INI adapter:\n\n```crystal\nrequire \"ini\"\n\nclass INIAdapter \u003c Totem::ConfigTypes::Adapter\n  def read(raw)\n    INI.parse(raw)\n  end\n\n  def write(io, config)\n    config.settings.each do |key, items|\n      next unless data = items.as_h?\n      io \u003c\u003c \"[\" \u003c\u003c key \u003c\u003c \"]\\n\"\n      data.each do |name, value|\n        io \u003c\u003c name \u003c\u003c \" = \" \u003c\u003c value \u003c\u003c \"\\n\"\n      end\n    end\n  end\nend\n\n# Do not forget register it\nTotem::ConfigTypes.register_adapter(\"ini\", INIAdapter.new)\n# Also you can set aliases\nTotem::ConfigTypes.register_alias(\"cnf\", \"ini\")\n```\n\nMore examples to review [built-in adapters](https://github.com/icyleaf/totem/blob/master/src/totem/config_types).\n\n### Write a remote provider\n\nCreating the custom remote provider by integration `Totem::RemoteProviders::Adapter` abstract class. Here has two methods must be implement:\n`read` and `get`, please reivew the [built-in remote providers](https://github.com/icyleaf/totem/blob/master/src/totem/remote_providers).\n\n## Q \u0026 A\n\n### How to debug?\n\nYou can use Crystal built-in `#pp` or `#pp!` method to prints a series of instance variables:\n\n```\n#\u003cTotem::Config\n @config_paths=[\"/etc/totem\", \"~/.totem\"],\n @config_name=\"config\",\n @config_type=\"json\",\n @key_delimiter=\".\",\n @automatic_env=false,\n @env_prefix=nil,\n @aliases={\"user\" =\u003e \"profile.user.name\"},\n @overrides={\"profile\" =\u003e {\"user\" =\u003e {\"gender\" =\u003e \"male\"}}, \"name\" =\u003e \"foo\"},\n @config={\"profile\" =\u003e {\"user\" =\u003e {\"gender\" =\u003e \"unkown\"}}, \"name\" =\u003e \"bar\"}},\n @env={\"name\" =\u003e \"TOTEM_NAME\"},\n @defaults={\"name\" =\u003e \"alana\"}\u003e\n```\n\n## Help and Discussion\n\nYou can browse the API documents:\n\nhttps://icyleaf.github.io/totem/\n\nYou can browse the Changelog:\n\nhttps://github.com/icyleaf/totem/blob/master/CHANGELOG.md\n\nIf you have found a bug, please create a issue here:\n\nhttps://github.com/icyleaf/totem/issues/new\n\n## How to Contribute\n\nYour contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list.\n\nAll [Contributors](https://github.com/icyleaf/totem/graphs/contributors) are on the wall.\n\n## You may also like\n\n- [halite](https://github.com/icyleaf/halite) - HTTP Requests Client with a chainable REST API, built-in sessions and middlewares.\n- [markd](https://github.com/icyleaf/markd) - Yet another markdown parser built for speed, Compliant to CommonMark specification.\n- [poncho](https://github.com/icyleaf/poncho) - A .env parser/loader improved for performance.\n- [popcorn](https://github.com/icyleaf/popcorn) - Easy and Safe casting from one type to another.\n- [fast-crystal](https://github.com/icyleaf/fast-crystal) - 💨 Writing Fast Crystal 😍 -- Collect Common Crystal idioms.\n\n## License\n\n[MIT License](https://github.com/icyleaf/totem/blob/master/LICENSE) © icyleaf\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficyleaf%2Ftotem","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficyleaf%2Ftotem","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficyleaf%2Ftotem/lists"}