1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
|
require_relative 'asetus/configstruct'
require_relative 'asetus/adapter/yaml'
require_relative 'asetus/adapter/json'
require_relative 'asetus/adapter/toml'
require 'fileutils'
class AsetusError < StandardError; end
class NoName < AsetusError; end
class UnknownOption < AsetusError; end
# @example common use case
# CFGS = Asetus.new :name=>'my_sweet_program' :load=>false # do not load config from filesystem
# CFGS.default.ssh.port = 22
# CFGS.default.ssh.hosts = %w(host1.example.com host2.example.com)
# CFGS.default.auth.user = lana
# CFGS.default.auth.password = dangerzone
# CFGS.load # load system config and user config from filesystem and merge with defaults to #cfg
# raise StandardError, 'edit ~/.config/my_sweet_program/config' if CFGS.create # create user config from default config if no system or user config exists
# # use the damn thing
# CFG = CFGS.cfg
# user = CFG.auth.user
# password = CFG.auth.password
# ssh_port = CFG.ssh.port
# ssh_hosts = CFG.ssh.hosts
class Asetus
CONFIG_FILE = 'config'
attr_reader :cfg, :default, :file
attr_accessor :system, :user
class << self
def cfg *args
new(*args).cfg
end
end
# When this is called, by default :system and :user are loaded from
# filesystem and merged with default, so that user overrides system which
# overrides default
#
# @param [Symbol] level which configuration level to load, by default :all
# @return [void]
def load(level = :all)
@cfg = merge @cfg, @default if %i[default all].include?(level)
if %i[system all].include?(level)
@system = load_cfg @sysdir
@cfg = merge @cfg, @system
end
return unless %i[user all].include?(level)
@user = load_cfg @usrdir
@cfg = merge @cfg, @user
end
# @param [Symbol] level which configuration level to save, by default :user
# @return [void]
def save(level = :user)
if level == :user
save_cfg @usrdir, @user
elsif level == :system
save_cfg @sysdir, @system
end
end
# @example create user config from default config and raise error, if no config was found
# raise StandardError, 'edit ~/.config/name/config' if asetus.create
# @param [Hash] opts options for Asetus
# @option opts [Symbol] :source source to use for settings to save, by default :default
# @option opts [Symbol] :destination destination to use for settings to save, by default :user
# @option opts [boolean] :load load config once saved, by default false
# @return [boolean] true if config didn't exist and was created, false if config already exists
def create(opts = {})
src = opts.delete :source
src ||= :default
dst = opts.delete :destination
dst ||= :user
no_config = false
no_config = true if @system.empty? && @user.empty?
if no_config
src = instance_variable_get '@' + src.to_s
instance_variable_set('@' + dst.to_s, src.dup)
save dst
load if opts.delete :load
end
no_config
end
private
# @param [Hash] opts options for Asetus.new
# @option opts [String] :name name to use for asetus (/etc/name/, ~/.config/name/) - autodetected if not defined
# @option opts [String] :adapter adapter to use 'yaml', 'json' or 'toml' for now
# @option opts [String] :usrdir directory for storing user config ~/.config/name/ by default
# @option opts [String] :sysdir directory for storing system config /etc/name/ by default
# @option opts [String] :cfgfile configuration filename, by default CONFIG_FILE
# @option opts [Hash] :default default settings to use
# @option opts [boolean] :load automatically load+merge system+user config with defaults in #cfg
# @option opts [boolean] :key_to_s convert keys to string by calling #to_s for keys
def initialize(opts = {})
@name = (opts.delete(:name) or metaname)
@adapter = (opts.delete(:adapter) or 'yaml')
@usrdir = (opts.delete(:usrdir) or File.join(Dir.home, '.config', @name))
@sysdir = (opts.delete(:sysdir) or File.join('/etc', @name))
@cfgfile = (opts.delete(:cfgfile) or CONFIG_FILE)
@default = ConfigStruct.new opts.delete(:default)
@system = ConfigStruct.new
@user = ConfigStruct.new
@cfg = ConfigStruct.new
@load = true
@load = opts.delete(:load) if opts.has_key?(:load)
@key_to_s = opts.delete(:key_to_s)
raise UnknownOption, "option '#{opts}' not recognized" unless opts.empty?
load :all if @load
end
def load_cfg(dir)
@file = File.join dir, @cfgfile
file = File.read @file
ConfigStruct.new(from(@adapter, file), key_to_s: @key_to_s)
rescue Errno::ENOENT
ConfigStruct.new
end
def save_cfg(dir, config)
config = to(@adapter, config)
file = File.join dir, @cfgfile
FileUtils.mkdir_p dir
File.write file, config
end
def merge *configs
hash = {}
configs.each do |config|
hash = hash._asetus_deep_merge config._asetus_to_hash
end
ConfigStruct.new hash
end
def from(adapter, string)
name = 'from_' + adapter
send name, string
end
def to(adapter, config)
name = 'to_' + adapter
send name, config
end
def metaname
path = caller_locations[-1].path
File.basename path, File.extname(path)
rescue StandardError
raise NoName, "can't figure out name, specify explicitly"
end
end
class Hash
def _asetus_deep_merge(newhash)
merger = proc do |_key, oldval, newval|
oldval.is_a?(Hash) && newval.is_a?(Hash) ? oldval.merge(newval, &merger) : newval
end
merge newhash, &merger
end
end
|