Giter Site home page Giter Site logo

totem's Introduction

totem-logo

Totem

Language Tag Build Status

Crystal configuration with spirit. Inspired from Go's viper. Totem Icon by lastspark from Noun Project.

Configuration file formats is always the problem, you want to focus on building awesome things. Totem is here to help with that.

Totem has following features:

  • Reading from JSON, YAML, dotenv formats config files or raw string.
  • Reading from environment variables.
  • Reading from remote key-value store systems(redis/etcd).
  • Provide a mechanism to set default values for your different configuration options.
  • Provide an alias system to easily rename parameters without breaking existing code.
  • Write configuration to file with JSON, YAML formats.
  • Convert config to struct with builder.

And we keep it minimize and require what you want with adapter and remote provider! No more dependenices what you do not need. Only JSON and YAML adapters were auto requires.

Uses the following precedence order. Each item takes precedence over the item below it:

  • alias
  • override, explicit call to set
  • env
  • config
  • kvstores
  • default

Totem configuration keys are case insensitive.

Installation

Add this to your application's shard.yml:

dependencies:
  totem:
    github: icyleaf/totem

Quick Start

require "totem"

Operating configuration

totem = Totem.new
totem.set_default("name", "foo")
totem.set_defaults({
  "age"    => 18,
  "gender" => "male",
  "hobbies" => [
    "skateboarding",
    "snowboarding",
    "go"
  ]
})

totem.get("name").as_s # => "foo"
totem.get("age").as_i # => 18
totem.set("name", "bar")
totem.alias(alias_key: "key", key: "name")
totem.get("name").as_s # => "bar"
totem.get("key").as_s # => "bar"

Loading configuration

Support JSON, YAML and dotenv data from raw string and file.

From raw string

Load yaml string

raw = <<-EOF
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
  pants:
    size: large
age: 35
eyes : brown
EOF

totem = Totem.from_yaml raw
totem.get("Hacker").as_bool                           # => true
totem.get("age").as_i                                 # => 35
totem.get("clothing").as_h["pants"].as_h["size"].as_s # => "large"

Load json string

raw = <<-EOF
{
  "id": "0001",
  "type": "donut",
  "name": "Cake",
  "ppu": 0.55,
  "batters": {
    "batter": [
      {
        "type": "Regular"
      },
      {
        "type": "Chocolate"
      },
      {
        "type": "Blueberry"
      },
      {
        "type": "Devil's Food"
      }
    ]
  }
}
EOF

totem = Totem.from_json raw
totem.get("name")                                         # => "Cake"
totem.get("ppu")                                          # => 0.55
totem.get("batters").as_h["batter"].as_a[0].as_h["type"]  # => "Regular"

Load dotenv string

Add poncho to shards.yml and require the adapter.

require "totem"
require "totem/config_types/env"    # Make sure you require

raw = <<-EOF
# COMMENTS=work
STR='foo'
STR_WITH_COMMENTS=bar         # str with comment
STR_WITH_HASH_SYMBOL="abc#123"#stick comment
INT=33
EOF

totem = Totem.from_env raw
totem.get("str")                    # => "foo"
totem.get("str_with_comments")      # => bar
totem.get("str_with_hash_symbol")   # => "abc#123"
totem.get("int")                    # => "33"

From file

Add poncho to shards.yml and require the adapter if you need load dotenv file.

# Load yaml file from file with path
totem = Totem.from_file "./spec/fixtures/config.yaml"

# Load json file from file with multi-paths
totem = Totem.from_file "config.yaml", ["/etc", ".", "./spec/fixtures"]

# Load dotenv file
totem = Totem.from_file "config.env"

Usage

Load configuration with multiple paths

Totem can search multiple paths, but currently a single Totem instance only supports a single configuration file.

totem = Totem.new("config", "/etc/totem/")  # => New a instance with name and path of config file
totem.config_paths << "~/.totem"            # => path to look for the config file in
totem.config_paths << "./config"            # => optionally look for config in the working directory
begin
  totem.load!                               # => Find and read the config file (order by yaml/yml/json/env)
rescue e
  puts "Fatal error config file: #{e.message}"
end

Set Alias and using alias

Aliases permit a single value to be referenced by multiple keys

totem.alias("nickname", "Name")

totem.set("name", "foo")
totem.set("nickname", "bar")

totem.get("name")       # => "foo"
totem.get("nickname")   # => "foo"

Working with nested key

All accessor methods accept nested key:

totem.set_default("profile.user.name", "foo")
totem.set("profile.user.age", 13)
totem.alias("username", "profile.user.name")
totem.bind_env("profile.user.nickname", "PROFILE_USER_NICKNAME")
totem.get("profile.user.age")

Working with environment variables

Totem has full support for environment variables, example:

ENV["ID"] = "123"
ENV["FOOD"] = "Pinapple"
ENV["NAME"] = "Polly"

totem = Totem.new

totem.bind_env("ID")
totem.get("id").as_i        # => 123

totem.bind_env("f", "FOOD")
totem.get("f").as_s         # => "Pinapple"

totem.automative_env
totem.get("name").as_s      # => "Polly"

Working with environment prefix:

totem.automative_env(prefix: "totem")
# Same as
# totem.env_prefix = "totem"
# totem.automative_env = true

totem.get("id").as_i    # => 123
totem.get("food").as_s  # => "Pinapple"
totem.get("name").as_s  # => "Polly"

Working with remote providers

Totem retrieve configuration from Key-Value store, which means that you can get your configuration values on the air. Avaliable providers is redis and etcd.

Use redis

It dependency crystal-redis shard. Install it before use.

require "totem"
require "totem/remote_providers/redis"

totem = Totem.new
totem.add_remote(provider: "redis", endpoint: "redis://localhost:6379/0")

totem.get("user:name")      # => "foo"
totem.get("user:id").as_i   # => 123

You can also get raw data from one key with path:

totem.config_type = "json"  # There is no file extension in a stream data, supported extensions are all registed config types in Totem.
totem.add_remote(provider: "redis", endpoint: "redis://localhost:6379/0", path: "config:development")

totem.get("user:name")      # => "foo"
totem.get("user:id").as_i   # => 123

Use etcd

It dependency etcd-crystal shard and ONLY works etcd v2 API. Install it before use.

require "totem"
require "totem/remote_providers/etcd"

totem = Totem.new
totem.add_remote(provider: "etcd", endpoint: "http://localhost:2379")

totem.get("user:name")      # => "foo"
totem.get("user:id").as_i   # => 123

You can also get raw data from one key with path:

totem.config_type = "yaml"  # There is no file extension in a stream data, supported extensions are all registed config types in Totem.
totem.add_remote(provider: "etcd", endpoint: "http://localhost:2379", path: "/config/development.yaml")

totem.get("user:name")      # => "foo"
totem.get("user:id").as_i   # => 123

Iterating configuration

Iterate in Totem is very easy, you can get #keys, #flat_keys, #settings (a.k.a #to_h) even iterating it directly with #each:

totem.settings    # => {"id" => 123, "user" => {"name" => "foobar", "age" => 20}}
totem.keys        # => ["id", "user"]
totem.flat_keys   # => ["id", "user.name", "user.age"]

totem.each do |key, value|
  # do something
end

Serialization

Serialize configuration to Struct, at current stage you can pass a JSON::Serializable/YAML::Serializable struct to mapping.

struct Profile
  include JSON::Serializable

  property name : String
  property hobbies : Array(String)
  property age : Int32
  property eyes : String
end

totem = Totem.from_file "spec/fixtures/config.yaml"
profile = totem.mapping(Profile)
profile.name      # => "steve"
profile.age       # => 35
profile.eyes      # => "brown"
profile.hobbies   # => ["skateboarding", "snowboarding", "go"]

Serialize configuration with part of key:

struct Clothes
  include JSON::Serializable

  property jacket : String
  property trousers : String
  property pants : Hash(String, String)
end

totem = Totem.from_file "spec/fixtures/config.yaml"
clothes = profile.mapping(Clothes, "clothing")
# => Clothes(@jacket="leather", @pants={"size" => "large"}, @trousers="denim")

Storing configuration to file

Simple to use #store! method.

raw = <<-EOF
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
  jacket: leather
  trousers: denim
  pants:
    size: large
age: 35
eyes : brown
EOF

totem = Totem.from_yaml raw
totem.set("nickname", "Freda")
totem.set("eyes", "blue")
totem.store!("profile.json")

Advanced Usage

Use config builder

You can generate a configuration with Totem builder with any Object.

struct Configuration
  include Totem::ConfigBuilder

  build do
    config_type "json"
    config_paths ["/etc/totem", "~/.config/totem", "config/"]
  end
end

config = Configuration.configure do |c|
  c.set_default "name", "foobar"
end

config["name"] # => "foobar"

The builder also could mapping config to struct.

struct Profile
  include Totem::ConfigBuilder

  property name : String
  property hobbies : Array(String)
  property age : Int32
  property eyes : String

  build do
    config_type "yaml"
    config_paths ["/etc/totem", "~/.config/totem", "config/"]
  end
end

profile = Profile.configure
profile.name          # => "steve"
profile["nested.key"] # => "foo"

Write a config adapter

Creating the custom adapter by integration Totem::ConfigTypes::Adapter abstract class. Here has two methods must be implement: read and write. For example, let us write a INI adapter:

require "ini"

class INIAdapter < Totem::ConfigTypes::Adapter
  def read(raw)
    INI.parse(raw)
  end

  def write(io, config)
    config.settings.each do |key, items|
      next unless data = items.as_h?
      io << "[" << key << "]\n"
      data.each do |name, value|
        io << name << " = " << value << "\n"
      end
    end
  end
end

# Do not forget register it
Totem::ConfigTypes.register_adapter("ini", INIAdapter.new)
# Also you can set aliases
Totem::ConfigTypes.register_alias("cnf", "ini")

More examples to review built-in adapters.

Write a remote provider

Creating the custom remote provider by integration Totem::RemoteProviders::Adapter abstract class. Here has two methods must be implement: read and get, please reivew the built-in remote providers.

Q & A

How to debug?

You can use Crystal built-in #pp or #pp! method to prints a series of instance variables:

#<Totem::Config
 @config_paths=["/etc/totem", "~/.totem"],
 @config_name="config",
 @config_type="json",
 @key_delimiter=".",
 @automatic_env=false,
 @env_prefix=nil,
 @aliases={"user" => "profile.user.name"},
 @overrides={"profile" => {"user" => {"gender" => "male"}}, "name" => "foo"},
 @config={"profile" => {"user" => {"gender" => "unkown"}}, "name" => "bar"}},
 @env={"name" => "TOTEM_NAME"},
 @defaults={"name" => "alana"}>

Help and Discussion

You can browse the API documents:

https://icyleaf.github.io/totem/

You can browse the Changelog:

https://github.com/icyleaf/totem/blob/master/CHANGELOG.md

If you have found a bug, please create a issue here:

https://github.com/icyleaf/totem/issues/new

How to Contribute

Your contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list.

All Contributors are on the wall.

You may also like

  • halite - HTTP Requests Client with a chainable REST API, built-in sessions and middlewares.
  • markd - Yet another markdown parser built for speed, Compliant to CommonMark specification.
  • poncho - A .env parser/loader improved for performance.
  • popcorn - Easy and Safe casting from one type to another.
  • fast-crystal - ๐Ÿ’จ Writing Fast Crystal ๐Ÿ˜ -- Collect Common Crystal idioms.

License

MIT License ยฉ icyleaf

totem's People

Contributors

dancrew32 avatar icyleaf avatar imgbotapp avatar wavell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

totem's Issues

nested yaml value

hello, thanks for the great lib, question re pulling a nested value from yaml config file,

config.yaml

sync:
  /tmp/file:
    remote_host: host1
    remote_path: /tmp/
    remote_user: joe
    rsync_opts: -azP
    interval: 10 
  
  /tmp/file2:
    remote_host: host2
    remote_path: /tmp
    remote_user: joe
    rsync_opts: -jSa
    interval: 60

how do I access a variable, for example, "interval" while parsing this in a loop,

  begin
    syncs = totem.get("sync").as_h
  rescue exception
    log.error("unable to read syncs")
    abort "unable to read syncs", 1
  end 

  channel = Channel(String).new

  syncs.each do |key,val|
    begin
      totem.set_default("syncs.key.interval", 3)
      interval = totem.get("sync.key.interval")
    rescue exception
      log.error("error running sync worker")
      abort exception, 1
    end
  end

its not picking up the "interval" variable in a loop, I also tried,

totem.get("sync.#{key}.interval")

but not able to access this value from a loop

Series of files.

Right now, I am doing:

File.open("#{CALLSITE}/#{FILE_CONFIGURATION}") do |io|
  @configuration.parse(io)
end

File.open("#{CALLSITE}/#{FILE_SECRETS}") do |io|
  @configuration.parse(io)
end

...after seeing what load_file! does, to get the special behavior rather than clear and make a new object; rather than .to_h and merging two configuration files, so I can have Totem::Config intact, but stacked with layered ( and possibly over-riding values )

Would be nice if there were an interface to this such that config_file could be an Array and if not, be cast as an Array to get the current behavior, but of a single item Array.

Right now, calling parse does seem to wipe out versus deeply merge hash values, attempting this special behavior.

Configuration adapter extension support

In the first version of totem and Go's viper both includes all dependencies, it slowed down compile speed and took more time to install dependencies, of cause more disk space. Then i think how to keep it minimize and less external dependency, the answer is change it to adapter extension.

before:

  1. Install all dependencies in shards.yml (for now, two is less: popcorn and poncho)
  2. Require all dependencies.
  3. Hard core to case and parse the raw.

after:

  1. Keep the minimize dependencies(only popcorn need to casting type of variable)
  2. Keep JSON/YAML adapters to auto required, because it's Crystal built-in.
  3. Require what you need adapter(eg, dotenv format depend on poncho shard)

Error: no overload matches 'Totem::Any.new' with type Totem::Any

Affects Totem 0.5.2 on Crystal 0.31.1. When compiling my project using Totem as a dependency, I get the following error:

In lib/totem/src/totem/any.cr:21:26

 21 | obj[key] = Any.new(value)
                     ^--
Error: no overload matches 'Totem::Any.new' with type Totem::Any

Overloads are:
 - Totem::Any.new(raw : Array(_))
 - Totem::Any.new(raw : Hash(String, _))
 - Totem::Any.new(raw : Type)

Adding the following initialize method overload to Totem::Any seems to resolve the problem:

    def initialize(raw : Any)
      @raw = raw.raw
    end

I'm not sure this is the ideal solution (I'm a complete Crystal newbie), but it does allow the code to compile and it seems to behave as expected.

Variable Substitution

A lot of configs allow variable substitution within the contents, to support DRY. Will totem ever support reading such config fully resolved? For manipulation of such config, to support edits, and then writing it back out, it will also need a parsing mode where substitution is purposefully not done.

Change `#fetch` method behavior

Before

def fetch(key, default_value = nil) : Any?
end

After

def fetch(key) : Any?
end

def fetch(key, default_value : Any | Anye::Type) : Any
end

unable to parse string with a dot

Hello, I am trying to open a yaml config with this structure,

sync:
  /home/user/dir1/nodot: 
    remote_host: web1 
    remote_path: /tmp/cache  

  /home/user/dir1/.withdot: 
    remote_host: web1 
    remote_path: /tmp/cache  

getting a lookup error, its tripping up over the dot in ".withdot" path.


sync./home/user/dir1/.withdot.remote_path

F, [2022-06-28 08:22:02 -04:00 #221113] FATAL -- : unable to get config values: Not found config: sync./home/user/dir1/.withdot.remote_path
unable to get config values: Not found config: sync./home/user/dir1/.withdot.remote_path

is there an escape char or substitution I can use for strings with dots in them?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.