asetus-0.4.0/0000755000004100000410000000000014451777276013073 5ustar www-datawww-dataasetus-0.4.0/README.md0000644000004100000410000000433414451777276014356 0ustar www-datawww-data# Asetus Configuration library for ruby with YAML/JSON/TOML backends with unified object access ## Install ``` % gem install asetus ``` ## Use ### Simple ``` require 'asetus' cfg = Asetus.cfg port = cfg.server.port user = cfg.auth.user pw = cfg.auth.password ``` It tried to detect your software name via caller_locations if no ':name' argument was given. It automatically loads /etc/name/config and ~/.config/name/config and merges them together. ### Advanced ``` require 'asetus' asetus = Asetus.new name: 'mykewlapp', default: {'poop'=>'xyzzy'}, adapter: 'yaml', usrdir: '/home/app/config/', sysdir: '/System/config/', load: false asetus.default.poop2 = [1, 2, 3, 4] asetus.default.starship.poopers = 42 asetus.load :user if asetus.user.empty? asetus.user = asetus.default asetus.save :user end asetus.load # load+merges cfg, takes argument :default, :system, :user asetus.cfg # merged default + system + user (merged on load) asetus.default # default only asetus.system # system only asetus.user # user only ``` ## Reserved methods * each - iterate all config keys in current level * has_key?(arg) - check if current level has key arg * [arg] - fetch arg (useful for non-literal retrieval, instead of using #send) * key? - all keys have question mark version reserved, checks if key exists+true (true), exists+false (false), not-exists (nil) + all object class methods ## TODO * should I add feature to raise on unconfigured/unset? * should I always merge to 'cfg' when default/system/config is set? ## License and Copyright Copyright 2014-2016 Saku Ytti Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. asetus-0.4.0/asetus.gemspec0000644000004100000410000000223714451777276015750 0ustar www-datawww-dataGem::Specification.new do |s| s.name = 'asetus' s.version = '0.4.0' s.licenses = ['Apache-2.0'] s.platform = Gem::Platform::RUBY s.authors = ['Saku Ytti'] s.email = %w[saku@ytti.fi] s.homepage = 'http://github.com/ytti/asetus' s.summary = 'configuration library' s.description = 'configuration library with object access to YAML/JSON/TOML backends' s.files = %x(git ls-files -z).split("\x0") s.executables = %w[] s.require_path = 'lib' s.metadata['rubygems_mfa_required'] = 'true' s.required_ruby_version = '>= 2.7' s.add_development_dependency 'bundler', '~> 2.2' s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'rubocop', '~> 1.48.0' s.add_development_dependency 'rubocop-minitest', '~> 0.29.0' s.add_development_dependency 'rubocop-rake', '~> 0.6.0' s.add_development_dependency 'simplecov', '~> 0.22.0' s.add_development_dependency 'simplecov-cobertura', '~> 2.1.0' s.add_development_dependency 'simplecov-html', '~> 0.12.3' end asetus-0.4.0/.rubocop.yml0000644000004100000410000000263114451777276015347 0ustar www-datawww-datainherit_from: .rubocop_todo.yml # Do not attempt to police vendored code AllCops: NewCops: enable TargetRubyVersion: 2.7 Exclude: - 'vendor/**/*' Style/StringLiterals: Enabled: false Style/FrozenStringLiteralComment: Enabled: false Layout/LineLength: Enabled: false Lint/AmbiguousRegexpLiteral: Enabled: false Lint/RaiseException: Enabled: true Lint/StructNewOverride: Enabled: true # Stick to verbose until https://bugs.ruby-lang.org/issues/10177 is closed. Style/PreferredHashMethods: EnforcedStyle: verbose Layout/HashAlignment: EnforcedHashRocketStyle: table EnforcedColonStyle: table Style/Not: Enabled: false # comply with @ytti's exacting specifications Style/CommandLiteral: EnforcedStyle: percent_x Style/FormatString: EnforcedStyle: percent Style/FormatStringToken: EnforcedStyle: unannotated Style/HashEachMethods: Enabled: true Style/HashTransformKeys: Enabled: true Style/HashTransformValues: Enabled: true Style/RescueModifier: Enabled: false Style/SymbolProc: Enabled: false Style/Documentation: Enabled: false Style/ParallelAssignment: Enabled: false Metrics/MethodLength: Max: 45 ## Metrics/AbcSize: ## Max: 28 Metrics/ClassLength: Max: 200 ## Metrics/CyclomaticComplexity: ## Max: 7 Metrics/BlockLength: Max: 150 Metrics/ParameterLists: Max: 6 Lint/EmptyBlock: Enabled: false require: - rubocop-rake - rubocop-minitest asetus-0.4.0/.gitignore0000644000004100000410000000002114451777276015054 0ustar www-datawww-dataGemfile.lock pkg asetus-0.4.0/.rubocop_todo.yml0000644000004100000410000000371414451777276016377 0ustar www-datawww-data# This configuration was generated by # `rubocop --auto-gen-config` # on 2023-04-24 11:45:31 UTC using RuboCop version 1.48.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 25 # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 9 # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 9 # Offense count: 1 # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. # NamePrefix: is_, has_, have_ # ForbiddenPrefixes: is_, has_, have_ # AllowedMethods: is_a? # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - 'spec/**/*' - 'lib/asetus/configstruct.rb' # Offense count: 1 Security/Eval: Exclude: - 'Rakefile' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Security/JSONLoad: Exclude: - 'lib/asetus/adapter/json.rb' # Offense count: 1 Style/MissingRespondToMissing: Exclude: - 'lib/asetus/configstruct.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: literals, strict Style/MutableConstant: Exclude: - 'lib/asetus.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: - 'lib/asetus/configstruct.rb' # Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: Exclude: - 'Rakefile' - 'lib/asetus.rb' asetus-0.4.0/Rakefile0000644000004100000410000000134214451777276014540 0ustar www-datawww-datarequire 'bundler/gem_tasks' require 'rake/testtask' require 'bundler' gemspec = eval(File.read(Dir['*.gemspec'].first)) gemfile = [gemspec.name, gemspec.version].join('-') + '.gem' desc 'Validate gemspec' task :gemspec do gemspec.validate end desc 'Run minitest' task :test do Rake::TestTask.new do |t| t.libs << 'spec' t.test_files = FileList['spec/**/*_spec.rb'] t.warning = true t.verbose = true end end desc 'Build gem' task :build desc 'Install gem' task install: :build do system "sudo -E sh -c 'umask 022; gem install gems/#{gemfile}'" end desc 'Remove gems' task :clean do FileUtils.rm_rf 'pkg' end desc 'Push to rubygems' task :push do system "gem push pkg/#{gemfile}" end task default: :test asetus-0.4.0/lib/0000755000004100000410000000000014451777276013641 5ustar www-datawww-dataasetus-0.4.0/lib/asetus/0000755000004100000410000000000014451777276015145 5ustar www-datawww-dataasetus-0.4.0/lib/asetus/configstruct.rb0000644000004100000410000000277614451777276020220 0ustar www-datawww-dataclass Asetus class ConfigStruct def _asetus_to_hash hash = {} @cfg.each do |key, value| value = value._asetus_to_hash if value.instance_of?(ConfigStruct) key = key.to_s if @key_to_s hash[key] = value end hash end def empty? @cfg.empty? end def each(&block) @cfg.each(&block) end def keys @cfg.keys end def has_key?(key) @cfg.has_key? key end private def initialize(hash = nil, opts = {}) @key_to_s = opts.delete :key_to_s @cfg = hash ? _asetus_from_hash(hash) : {} end def method_missing name, *args name = name.to_s name = args.shift if name[0..1] == '[]' # asetus.cfg['foo'] arg = args.first if name[-1..-1] == '?' # asetus.cfg.foo.bar? @cfg[name[0..-2]] if @cfg.has_key? name[0..-2] elsif name[-1..-1] == '=' # asetus.cfg.foo.bar = 'quux' _asetus_set name[0..-2], arg else _asetus_get name, arg # asetus.cfg.foo.bar end end def _asetus_set(key, value) @cfg[key] = value end def _asetus_get(key, _value) if @cfg.has_key? key @cfg[key] else @cfg[key] = ConfigStruct.new end end def _asetus_from_hash(hash) cfg = {} hash.each do |key, value| value = ConfigStruct.new value, key_to_s: @key_to_s if value.instance_of?(Hash) cfg[key] = value end cfg end end end asetus-0.4.0/lib/asetus/adapter/0000755000004100000410000000000014451777276016565 5ustar www-datawww-dataasetus-0.4.0/lib/asetus/adapter/json.rb0000644000004100000410000000062614451777276020067 0ustar www-datawww-dataclass Asetus def to_json(config) Adapter::JSON.to config._asetus_to_hash end def from_json(json) Adapter::JSON.from json end class Adapter class JSON class << self def to(hash) require 'json' ::JSON.pretty_generate hash end def from(json) require 'json' ::JSON.load json end end end end end asetus-0.4.0/lib/asetus/adapter/toml.rb0000644000004100000410000000063314451777276020067 0ustar www-datawww-dataclass Asetus def to_toml(config) Adapter::TOML.to config._asetus_to_hash end def from_toml(toml) Adapter::TOML.from toml end class Adapter class TOML class << self def to(hash) require 'toml' ::TOML::Generator.new(hash).body end def from(toml) require 'toml' ::TOML.load toml end end end end end asetus-0.4.0/lib/asetus/adapter/yaml.rb0000644000004100000410000000062214451777276020054 0ustar www-datawww-dataclass Asetus def to_yaml(config) Adapter::YAML.to config._asetus_to_hash end def from_yaml(yaml) Adapter::YAML.from yaml end class Adapter class YAML class << self def to(hash) require 'yaml' ::YAML.dump hash end def from(yaml) require 'yaml' ::YAML.unsafe_load yaml end end end end end asetus-0.4.0/lib/asetus.rb0000644000004100000410000001300514451777276015471 0ustar www-datawww-datarequire_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 asetus-0.4.0/Gemfile0000644000004100000410000000004714451777276014367 0ustar www-datawww-datasource 'https://rubygems.org' gemspec asetus-0.4.0/.github/0000755000004100000410000000000014451777276014433 5ustar www-datawww-dataasetus-0.4.0/.github/dependabot.yml0000644000004100000410000000131614451777276017264 0ustar www-datawww-data# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" target-branch: "master" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 10 labels: - dependencies - package-ecosystem: "bundler" target-branch: "master" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 10 labels: - dependencies asetus-0.4.0/.github/workflows/0000755000004100000410000000000014451777276016470 5ustar www-datawww-dataasetus-0.4.0/.github/workflows/stale.yml0000644000004100000410000000040214451777276020317 0ustar www-datawww-dataname: "Stale Issue/PR cleanup" on: schedule: - cron: "30 1 * * *" permissions: issues: write pull-requests: write jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v8 with: operations-per-run: 500 asetus-0.4.0/.github/workflows/ruby.yml0000644000004100000410000000252614451777276020201 0ustar www-datawww-data# This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby name: CI on: [ push, pull_request ] # push: # branches: [ master ] # pull_request: # branches: [ master ] jobs: test: runs-on: ubuntu-latest strategy: matrix: ruby-version: ['2.7', '3.0', '3.1', '3.2'] steps: - uses: actions/checkout@v3 - name: Set up Ruby # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, # change this to (see https://github.com/ruby/setup-ruby#versioning): uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: rubocop uses: reviewdog/action-rubocop@v2 with: rubocop_version: gemfile rubocop_extensions: rubocop-minitest:gemfile rubocop-rake:gemfile reporter: github-pr-review - name: Run tests run: bundle exec rake - uses: codecov/codecov-action@v3 if: ${{ always() }}