1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
5 # When loading YAML, deserialize :foo as ":foo", rather than raising
6 # "Psych::DisallowedClass: Tried to load unspecified class: Symbol"
7 class Psych::ScalarScanner
8 alias :orig_tokenize :tokenize
10 return string if string =~ /^:[a-zA-Z]/
17 class YAMLTree < Psych::Visitors::Visitor
18 def visit_ActiveSupport_Duration o
22 outstr += "#{seconds / 3600}h"
23 seconds = seconds % 3600
26 outstr += "#{seconds / 60}m"
27 seconds = seconds % 60
30 outstr += "#{seconds}s"
35 @emitter.scalar outstr, nil, nil, true, false, Nodes::Scalar::ANY
38 def visit_URI_Generic o
39 @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
43 @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
47 @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
55 class TrueClass; include Boolean; end
56 class FalseClass; include Boolean; end
58 class NonemptyString < String
63 @config_migrate_map = {}
67 def declare_config(assign_to, configtype, migrate_from=nil, migrate_fn=nil)
69 @config_migrate_map[migrate_from] = migrate_fn || ->(cfg, k, v) {
70 ConfigLoader.set_cfg cfg, assign_to, v
73 @config_types[assign_to] = configtype
77 def migrate_config from_config, to_config
79 from_config.each do |k, v|
80 if @config_migrate_map[k.to_sym]
81 @config_migrate_map[k.to_sym].call to_config, k, v
89 def coercion_and_check check_cfg, check_nonempty: true
90 @config_types.each do |cfgkey, cfgtype|
103 raise "missing #{cfgkey}"
106 if cfgtype == String and !cfg[k]
110 if cfgtype == String and cfg[k].is_a? Symbol
114 if cfgtype == Pathname and cfg[k].is_a? String
117 cfg[k] = Pathname.new("")
119 cfg[k] = Pathname.new(cfg[k])
121 raise "#{cfgkey} path #{cfg[k]} does not exist"
126 if cfgtype == NonemptyString
127 if (!cfg[k] || cfg[k] == "") && check_nonempty
128 raise "#{cfgkey} cannot be empty"
130 if cfg[k].is_a? String
135 if cfgtype == ActiveSupport::Duration
136 if cfg[k].is_a? Integer
137 cfg[k] = cfg[k].seconds
138 elsif cfg[k].is_a? String
139 cfg[k] = ConfigLoader.parse_duration(cfg[k], cfgkey: cfgkey)
147 if cfgtype == Integer && cfg[k].is_a?(String)
148 v = cfg[k].sub(/B\s*$/, '')
149 if mt = /(-?\d*\.?\d+)\s*([KMGTPE]i?)$/.match(v)
162 "T" => 1000000000000,
164 "P" => 1000000000000000,
166 "E" => 1000000000000000000,
172 if !cfg[k].is_a? cfgtype
173 raise "#{cfgkey} expected #{cfgtype} but was #{cfg[k].class}"
178 def self.set_cfg cfg, k, v
179 # "foo.bar = baz" --> { cfg["foo"]["bar"] = baz }
193 def self.parse_duration(durstr, cfgkey:)
196 durstr = durstr[1..-1]
199 duration_re = /(\d+(\.\d+)?)(s|m|h)/
202 mt = duration_re.match durstr
204 raise "#{cfgkey} not a valid duration: '#{durstr}', accepted suffixes are s, m, h"
206 multiplier = {s: 1, m: 60, h: 3600}
207 dursec += (Float(mt[1]) * multiplier[mt[3].to_sym] * sign)
208 durstr = durstr[mt[0].length..-1]
210 return dursec.seconds
213 def self.copy_into_config src, dst
215 dst.send "#{k}=", self.to_OrderedOptions(v)
219 def self.to_OrderedOptions confs
221 opts = ActiveSupport::OrderedOptions.new
223 opts[k] = self.to_OrderedOptions(v)
226 elsif confs.is_a? Array
227 confs.map { |v| self.to_OrderedOptions v }
233 def self.load path, erb: false
237 yaml = ERB.new(yaml).result(binding)