14287: Merge branch 'master' into 14287-federated-list
[arvados.git] / apps / workbench / lib / config_loader.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 module Psych
6   module Visitors
7     class YAMLTree < Psych::Visitors::Visitor
8       def visit_ActiveSupport_Duration o
9         seconds = o.to_i
10         outstr = ""
11         if seconds / 3600 > 0
12           outstr += "#{seconds / 3600}h"
13           seconds = seconds % 3600
14         end
15         if seconds / 60 > 0
16           outstr += "#{seconds / 60}m"
17           seconds = seconds % 60
18         end
19         if seconds > 0
20           outstr += "#{seconds}s"
21         end
22         if outstr == ""
23           outstr = "0s"
24         end
25         @emitter.scalar outstr, nil, nil, true, false, Nodes::Scalar::ANY
26       end
27
28       def visit_URI_Generic o
29         @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
30       end
31
32       def visit_URI_HTTP o
33         @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
34       end
35
36       def visit_Pathname o
37         @emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
38       end
39     end
40   end
41 end
42
43
44 module Boolean; end
45 class TrueClass; include Boolean; end
46 class FalseClass; include Boolean; end
47
48 class NonemptyString < String
49 end
50
51 class ConfigLoader
52   def initialize
53     @config_migrate_map = {}
54     @config_types = {}
55   end
56
57   def declare_config(assign_to, configtype, migrate_from=nil, migrate_fn=nil)
58     if migrate_from
59       @config_migrate_map[migrate_from] = migrate_fn || ->(cfg, k, v) {
60         ConfigLoader.set_cfg cfg, assign_to, v
61       }
62     end
63     @config_types[assign_to] = configtype
64   end
65
66
67   def migrate_config from_config, to_config
68     remainders = {}
69     from_config.each do |k, v|
70       if @config_migrate_map[k.to_sym]
71         begin
72           @config_migrate_map[k.to_sym].call to_config, k, v
73         rescue => e
74           raise "Error migrating '#{k}: #{v}' got error #{e}"
75         end
76       else
77         remainders[k] = v
78       end
79     end
80     remainders
81   end
82
83   def coercion_and_check check_cfg, check_nonempty: true
84     @config_types.each do |cfgkey, cfgtype|
85       begin
86         cfg = check_cfg
87         k = cfgkey
88         ks = k.split '.'
89         k = ks.pop
90         ks.each do |kk|
91           cfg = cfg[kk]
92           if cfg.nil?
93             break
94           end
95         end
96
97         if cfg.nil?
98           raise "missing #{cfgkey}"
99         end
100
101         if cfgtype == String and !cfg[k]
102           cfg[k] = ""
103         end
104
105         if cfgtype == String and cfg[k].is_a? Symbol
106           cfg[k] = cfg[k].to_s
107         end
108
109         if cfgtype == Pathname and cfg[k].is_a? String
110
111           if cfg[k] == ""
112             cfg[k] = Pathname.new("")
113           else
114             cfg[k] = Pathname.new(cfg[k])
115             if !cfg[k].exist?
116               raise "#{cfgkey} path #{cfg[k]} does not exist"
117             end
118           end
119         end
120
121         if cfgtype == NonemptyString
122           if (!cfg[k] || cfg[k] == "") && check_nonempty
123             raise "#{cfgkey} cannot be empty"
124           end
125           if cfg[k].is_a? String
126             next
127           end
128         end
129
130         if cfgtype == ActiveSupport::Duration
131           if cfg[k].is_a? Integer
132             cfg[k] = cfg[k].seconds
133           elsif cfg[k].is_a? String
134             cfg[k] = ConfigLoader.parse_duration(cfg[k], cfgkey: cfgkey)
135           end
136         end
137
138         if cfgtype == URI
139           if cfg[k]
140             cfg[k] = URI(cfg[k])
141           else
142             cfg[k] = URI("")
143           end
144         end
145
146         if cfgtype == Integer && cfg[k].is_a?(String)
147           v = cfg[k].sub(/B\s*$/, '')
148           if mt = /(-?\d*\.?\d+)\s*([KMGTPE]i?)$/.match(v)
149             if mt[1].index('.')
150               v = mt[1].to_f
151             else
152               v = mt[1].to_i
153             end
154             cfg[k] = v * {
155               'K' => 1000,
156               'Ki' => 1 << 10,
157               'M' => 1000000,
158               'Mi' => 1 << 20,
159               "G" =>  1000000000,
160               "Gi" => 1 << 30,
161               "T" =>  1000000000000,
162               "Ti" => 1 << 40,
163               "P" =>  1000000000000000,
164               "Pi" => 1 << 50,
165               "E" =>  1000000000000000000,
166               "Ei" => 1 << 60,
167             }[mt[2]]
168           end
169         end
170
171       rescue => e
172         raise "#{cfgkey} expected #{cfgtype} but '#{cfg[k]}' got error #{e}"
173       end
174
175       if !cfg[k].is_a? cfgtype
176         raise "#{cfgkey} expected #{cfgtype} but was #{cfg[k].class}"
177       end
178     end
179   end
180
181   def self.set_cfg cfg, k, v
182     # "foo.bar = baz" --> { cfg["foo"]["bar"] = baz }
183     ks = k.split '.'
184     k = ks.pop
185     ks.each do |kk|
186       cfg = cfg[kk]
187       if cfg.nil?
188         break
189       end
190     end
191     if !cfg.nil?
192       cfg[k] = v
193     end
194   end
195
196   def self.parse_duration durstr, cfgkey:
197     duration_re = /-?(\d+(\.\d+)?)(s|m|h)/
198     dursec = 0
199     while durstr != ""
200       mt = duration_re.match durstr
201       if !mt
202         raise "#{cfgkey} not a valid duration: '#{durstr}', accepted suffixes are s, m, h"
203       end
204       multiplier = {s: 1, m: 60, h: 3600}
205       dursec += (Float(mt[1]) * multiplier[mt[3].to_sym])
206       durstr = durstr[mt[0].length..-1]
207     end
208     return dursec.seconds
209   end
210
211   def self.copy_into_config src, dst
212     src.each do |k, v|
213       dst.send "#{k}=", self.to_OrderedOptions(v)
214     end
215   end
216
217   def self.to_OrderedOptions confs
218     if confs.is_a? Hash
219       opts = ActiveSupport::OrderedOptions.new
220       confs.each do |k,v|
221         opts[k] = self.to_OrderedOptions(v)
222       end
223       opts
224     elsif confs.is_a? Array
225       confs.map { |v| self.to_OrderedOptions v }
226     else
227       confs
228     end
229   end
230
231   def self.load path, erb: false
232     if File.exist? path
233       yaml = IO.read path
234       if erb
235         yaml = ERB.new(yaml).result(binding)
236       end
237       YAML.load(yaml, deserialize_symbols: false)
238     else
239       {}
240     end
241   end
242
243 end