runger_config 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +562 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +1121 -0
  5. data/lib/anyway/auto_cast.rb +53 -0
  6. data/lib/anyway/config.rb +473 -0
  7. data/lib/anyway/dynamic_config.rb +31 -0
  8. data/lib/anyway/ejson_parser.rb +40 -0
  9. data/lib/anyway/env.rb +73 -0
  10. data/lib/anyway/ext/deep_dup.rb +48 -0
  11. data/lib/anyway/ext/deep_freeze.rb +44 -0
  12. data/lib/anyway/ext/flatten_names.rb +37 -0
  13. data/lib/anyway/ext/hash.rb +40 -0
  14. data/lib/anyway/ext/string_constantize.rb +24 -0
  15. data/lib/anyway/loaders/base.rb +21 -0
  16. data/lib/anyway/loaders/doppler.rb +63 -0
  17. data/lib/anyway/loaders/ejson.rb +89 -0
  18. data/lib/anyway/loaders/env.rb +18 -0
  19. data/lib/anyway/loaders/yaml.rb +84 -0
  20. data/lib/anyway/loaders.rb +79 -0
  21. data/lib/anyway/option_parser_builder.rb +29 -0
  22. data/lib/anyway/optparse_config.rb +92 -0
  23. data/lib/anyway/rails/autoload.rb +42 -0
  24. data/lib/anyway/rails/config.rb +23 -0
  25. data/lib/anyway/rails/loaders/credentials.rb +64 -0
  26. data/lib/anyway/rails/loaders/secrets.rb +37 -0
  27. data/lib/anyway/rails/loaders/yaml.rb +9 -0
  28. data/lib/anyway/rails/loaders.rb +5 -0
  29. data/lib/anyway/rails/settings.rb +83 -0
  30. data/lib/anyway/rails.rb +24 -0
  31. data/lib/anyway/railtie.rb +28 -0
  32. data/lib/anyway/rbs.rb +92 -0
  33. data/lib/anyway/settings.rb +111 -0
  34. data/lib/anyway/testing/helpers.rb +36 -0
  35. data/lib/anyway/testing.rb +13 -0
  36. data/lib/anyway/tracing.rb +188 -0
  37. data/lib/anyway/type_casting.rb +144 -0
  38. data/lib/anyway/utils/deep_merge.rb +21 -0
  39. data/lib/anyway/utils/which.rb +18 -0
  40. data/lib/anyway/version.rb +5 -0
  41. data/lib/anyway.rb +3 -0
  42. data/lib/anyway_config.rb +54 -0
  43. data/lib/generators/anyway/app_config/USAGE +9 -0
  44. data/lib/generators/anyway/app_config/app_config_generator.rb +17 -0
  45. data/lib/generators/anyway/config/USAGE +13 -0
  46. data/lib/generators/anyway/config/config_generator.rb +51 -0
  47. data/lib/generators/anyway/config/templates/config.rb.tt +12 -0
  48. data/lib/generators/anyway/config/templates/config.yml.tt +13 -0
  49. data/lib/generators/anyway/install/USAGE +4 -0
  50. data/lib/generators/anyway/install/install_generator.rb +47 -0
  51. data/lib/generators/anyway/install/templates/application_config.rb.tt +17 -0
  52. data/sig/anyway_config.rbs +149 -0
  53. data/sig/manifest.yml +6 -0
  54. metadata +202 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module AutoCast
5
+ # Regexp to detect array values
6
+ # Array value is a values that contains at least one comma
7
+ # and doesn't start/end with quote or curly braces
8
+ ARRAY_RXP = /\A[^'"{].*\s*,\s*.*[^'"}]\z/
9
+
10
+ class << self
11
+ def call(val)
12
+ return val unless val.is_a?(::Hash) || val.is_a?(::String)
13
+
14
+ case val
15
+ when Hash
16
+ val.transform_values { call(_1) }
17
+ when ARRAY_RXP
18
+ val.split(/\s*,\s*/).map { call(_1) }
19
+ when /\A(true|t|yes|y)\z/i
20
+ true
21
+ when /\A(false|f|no|n)\z/i
22
+ false
23
+ when /\A(nil|null)\z/i
24
+ nil
25
+ when /\A\d+\z/
26
+ val.to_i
27
+ when /\A\d*\.\d+\z/
28
+ val.to_f
29
+ when /\A['"].*['"]\z/
30
+ val.gsub(/(\A['"]|['"]\z)/, "")
31
+ else
32
+ val
33
+ end
34
+ end
35
+
36
+ def cast_hash(obj)
37
+ obj.transform_values do |val|
38
+ val.is_a?(::Hash) ? cast_hash(val) : call(val)
39
+ end
40
+ end
41
+
42
+ def coerce(_key, val)
43
+ call(val)
44
+ end
45
+ end
46
+ end
47
+
48
+ module NoCast
49
+ def self.call(val) = val
50
+
51
+ def self.coerce(_key, val) = val
52
+ end
53
+ end
@@ -0,0 +1,473 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway/optparse_config"
4
+ require "anyway/dynamic_config"
5
+
6
+ module Anyway # :nodoc:
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::DeepFreeze
10
+ using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
12
+
13
+ using(Module.new do
14
+ refine Object do
15
+ def vm_object_id() = (object_id << 1).to_s(16)
16
+ end
17
+ end)
18
+
19
+ # Base config class
20
+ # Provides `attr_config` method to describe
21
+ # configuration parameters and set defaults
22
+ class Config
23
+ PARAM_NAME = /^[a-z_](\w+)?$/
24
+
25
+ # List of names that couldn't be used as config names
26
+ # (the class instance methods we use)
27
+ RESERVED_NAMES = %i[
28
+ config_name
29
+ env_prefix
30
+ as_env
31
+ values
32
+ class
33
+ clear
34
+ deconstruct_keys
35
+ dig
36
+ dup
37
+ initialize
38
+ load
39
+ load_from_sources
40
+ option_parser
41
+ pretty_print
42
+ raise_validation_error
43
+ reload
44
+ resolve_config_path
45
+ tap
46
+ to_h
47
+ to_source_trace
48
+ write_config_attr
49
+ __type_caster__
50
+ ].freeze
51
+
52
+ class Error < StandardError; end
53
+
54
+ class ValidationError < Error; end
55
+
56
+ include OptparseConfig
57
+ include DynamicConfig
58
+
59
+ class BlockCallback
60
+ attr_reader :block
61
+
62
+ def initialize(block)
63
+ @block = block
64
+ end
65
+
66
+ def apply_to(config)
67
+ config.instance_exec(&block)
68
+ end
69
+ end
70
+
71
+ class NamedCallback
72
+ attr_reader :name
73
+
74
+ def initialize(name)
75
+ @name = name
76
+ end
77
+
78
+ def apply_to(config) = config.send(name)
79
+ end
80
+
81
+ class << self
82
+ def attr_config(*args, **hargs)
83
+ new_defaults = hargs.deep_dup
84
+ new_defaults.stringify_keys!
85
+
86
+ defaults.merge! new_defaults
87
+
88
+ new_keys = ((args + new_defaults.keys) - config_attributes)
89
+
90
+ validate_param_names! new_keys.map(&:to_s)
91
+
92
+ new_keys.map!(&:to_sym)
93
+
94
+ unless (reserved_names = (new_keys & RESERVED_NAMES)).empty?
95
+ raise ArgumentError, "Can not use the following reserved names as config attrubutes: " \
96
+ "#{reserved_names.sort.map(&:to_s).join(", ")}"
97
+ end
98
+
99
+ config_attributes.push(*new_keys)
100
+
101
+ define_config_accessor(*new_keys)
102
+
103
+ # Define predicate methods ("param?") for attributes
104
+ # having `true` or `false` as default values
105
+ new_defaults.each do |key, val|
106
+ next unless val.is_a?(TrueClass) || val.is_a?(FalseClass)
107
+ alias_method :"#{key}?", :"#{key}"
108
+ end
109
+ end
110
+
111
+ def defaults
112
+ return @defaults if instance_variable_defined?(:@defaults)
113
+
114
+ @defaults = if superclass < Anyway::Config
115
+ superclass.defaults.deep_dup
116
+ else
117
+ new_empty_config
118
+ end
119
+ end
120
+
121
+ def config_attributes
122
+ return @config_attributes if instance_variable_defined?(:@config_attributes)
123
+
124
+ @config_attributes = if superclass < Anyway::Config
125
+ superclass.config_attributes.dup
126
+ else
127
+ []
128
+ end
129
+ end
130
+
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
+ raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
+
135
+ return unless Settings.matching_env?(env)
136
+
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
139
+ end
140
+
141
+ def required_attributes
142
+ return @required_attributes if instance_variable_defined?(:@required_attributes)
143
+
144
+ @required_attributes = if superclass < Anyway::Config
145
+ superclass.required_attributes.dup
146
+ else
147
+ []
148
+ end
149
+ end
150
+
151
+ def on_load(*names, &block)
152
+ raise ArgumentError, "Either methods or block should be specified, not both" if block && !names.empty?
153
+
154
+ if block
155
+ load_callbacks << BlockCallback.new(block)
156
+ else
157
+ load_callbacks.push(*names.map { NamedCallback.new(_1) })
158
+ end
159
+ end
160
+
161
+ def load_callbacks
162
+ return @load_callbacks if instance_variable_defined?(:@load_callbacks)
163
+
164
+ @load_callbacks = if superclass <= Anyway::Config
165
+ superclass.load_callbacks.dup
166
+ else
167
+ []
168
+ end
169
+ end
170
+
171
+ def config_name(val = nil)
172
+ return (@explicit_config_name = val.to_s) unless val.nil?
173
+
174
+ return @config_name if instance_variable_defined?(:@config_name)
175
+
176
+ @config_name = explicit_config_name || build_config_name
177
+ end
178
+
179
+ def explicit_config_name
180
+ return @explicit_config_name if instance_variable_defined?(:@explicit_config_name)
181
+
182
+ @explicit_config_name =
183
+ if superclass.respond_to?(:explicit_config_name)
184
+ superclass.explicit_config_name
185
+ end
186
+ end
187
+
188
+ def explicit_config_name?() = !explicit_config_name.nil?
189
+
190
+ def env_prefix(val = nil)
191
+ return (@env_prefix = val.to_s.upcase) unless val.nil?
192
+
193
+ return @env_prefix if instance_variable_defined?(:@env_prefix)
194
+
195
+ @env_prefix = if superclass < Anyway::Config && superclass.explicit_config_name?
196
+ superclass.env_prefix
197
+ else
198
+ config_name.upcase
199
+ end
200
+ end
201
+
202
+ def loader_options(val = nil)
203
+ return (@loader_options = val) unless val.nil?
204
+
205
+ return @loader_options if instance_variable_defined?(:@loader_options)
206
+
207
+ @loader_options = if superclass < Anyway::Config
208
+ superclass.loader_options
209
+ else
210
+ {}
211
+ end
212
+ end
213
+
214
+ def new_empty_config() = {}
215
+
216
+ def coerce_types(mapping)
217
+ Utils.deep_merge!(coercion_mapping, mapping)
218
+
219
+ mapping.each do |key, val|
220
+ type = val.is_a?(::Hash) ? val[:type] : val
221
+ next if type != :boolean
222
+
223
+ alias_method :"#{key}?", :"#{key}"
224
+ end
225
+ end
226
+
227
+ def coercion_mapping
228
+ return @coercion_mapping if instance_variable_defined?(:@coercion_mapping)
229
+
230
+ @coercion_mapping = if superclass < Anyway::Config
231
+ superclass.coercion_mapping.deep_dup
232
+ else
233
+ {}
234
+ end
235
+ end
236
+
237
+ def type_caster(val = nil)
238
+ return @type_caster unless val.nil?
239
+
240
+ @type_caster ||=
241
+ if coercion_mapping.empty?
242
+ fallback_type_caster
243
+ else
244
+ ::Anyway::TypeCaster.new(coercion_mapping, fallback: fallback_type_caster)
245
+ end
246
+ end
247
+
248
+ def fallback_type_caster(val = nil)
249
+ return (@fallback_type_caster = val) unless val.nil?
250
+
251
+ return @fallback_type_caster if instance_variable_defined?(:@fallback_type_caster)
252
+
253
+ @fallback_type_caster = if superclass < Anyway::Config
254
+ superclass.fallback_type_caster.deep_dup
255
+ else
256
+ ::Anyway::AutoCast
257
+ end
258
+ end
259
+
260
+ def disable_auto_cast!
261
+ @fallback_type_caster = ::Anyway::NoCast
262
+ end
263
+
264
+ private
265
+
266
+ def define_config_accessor(*names)
267
+ names.each do |name|
268
+ accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
269
+ def #{name}=(val)
270
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
271
+ values[:#{name}] = val
272
+ end
273
+
274
+ def #{name}
275
+ values[:#{name}]
276
+ end
277
+ RUBY
278
+ end
279
+ end
280
+
281
+ def accessors_module
282
+ return @accessors_module if instance_variable_defined?(:@accessors_module)
283
+
284
+ @accessors_module = Module.new.tap do |mod|
285
+ include mod
286
+ end
287
+ end
288
+
289
+ def build_config_name
290
+ unless name
291
+ raise "Please, specify config name explicitly for anonymous class " \
292
+ "via `config_name :my_config`"
293
+ end
294
+
295
+ # handle two cases:
296
+ # - SomeModule::Config => "some_module"
297
+ # - SomeConfig => "some"
298
+ unless name =~ /^(\w+)(::)?Config$/
299
+ raise "Couldn't infer config name, please, specify it explicitly " \
300
+ "via `config_name :my_config`"
301
+ end
302
+
303
+ # TODO(v3.0): Replace downcase with underscore
304
+ Regexp.last_match[1].tap(&:downcase!)
305
+ end
306
+
307
+ def validate_param_names!(names)
308
+ invalid_names = names.reject { |name| name =~ PARAM_NAME }
309
+ return if invalid_names.empty?
310
+
311
+ raise ArgumentError, "Invalid attr_config name: #{invalid_names.join(", ")}.\n" \
312
+ "Valid names must satisfy /#{PARAM_NAME.source}/."
313
+ end
314
+ end
315
+
316
+ on_load :validate_required_attributes!
317
+
318
+ attr_reader :config_name, :env_prefix
319
+
320
+ # Instantiate config instance.
321
+ #
322
+ # Example:
323
+ #
324
+ # my_config = Anyway::Config.new()
325
+ #
326
+ # # provide some values explicitly
327
+ # my_config = Anyway::Config.new({some: :value})
328
+ #
329
+ def initialize(overrides = nil)
330
+ @config_name = self.class.config_name
331
+
332
+ raise ArgumentError, "Config name is missing" unless @config_name
333
+
334
+ @env_prefix = self.class.env_prefix
335
+ @values = {}
336
+
337
+ load(overrides)
338
+ end
339
+
340
+ def reload(overrides = nil)
341
+ clear
342
+ load(overrides)
343
+ self
344
+ end
345
+
346
+ def clear
347
+ values.clear
348
+ @__trace__ = nil
349
+ self
350
+ end
351
+
352
+ def load(overrides = nil)
353
+ base_config = self.class.defaults.deep_dup
354
+
355
+ trace = Tracing.capture do
356
+ Tracing.trace!(:defaults) { base_config }
357
+
358
+ config_path = resolve_config_path(config_name, env_prefix)
359
+
360
+ load_from_sources(
361
+ base_config,
362
+ name: config_name,
363
+ env_prefix:,
364
+ config_path:,
365
+ **self.class.loader_options
366
+ )
367
+
368
+ if overrides
369
+ Tracing.trace!(:load) { overrides }
370
+
371
+ Utils.deep_merge!(base_config, overrides)
372
+ end
373
+ end
374
+
375
+ base_config.each do |key, val|
376
+ write_config_attr(key.to_sym, val)
377
+ end
378
+
379
+ # Trace may contain unknown attributes
380
+ trace&.keep_if { |key| self.class.config_attributes.include?(key.to_sym) }
381
+
382
+ # Run on_load callbacks
383
+ self.class.load_callbacks.each { _1.apply_to(self) }
384
+
385
+ # Set trace after we write all the values to
386
+ # avoid changing the source to accessor
387
+ @__trace__ = trace
388
+
389
+ self
390
+ end
391
+
392
+ def load_from_sources(base_config, **options)
393
+ Anyway.loaders.each do |(_id, loader)|
394
+ Utils.deep_merge!(base_config, loader.call(**options))
395
+ end
396
+ base_config
397
+ end
398
+
399
+ def dig(*keys) = values.dig(*keys)
400
+
401
+ def to_h() = values.deep_dup.deep_freeze
402
+
403
+ def dup
404
+ self.class.allocate.tap do |new_config|
405
+ %i[config_name env_prefix __trace__].each do |ivar|
406
+ new_config.instance_variable_set(:"@#{ivar}", send(ivar).dup)
407
+ end
408
+ new_config.instance_variable_set(:@values, values.deep_dup)
409
+ end
410
+ end
411
+
412
+ def resolve_config_path(name, env_prefix)
413
+ Anyway.env.fetch(env_prefix).delete("conf") || Settings.default_config_path.call(name)
414
+ end
415
+
416
+ def deconstruct_keys(keys) = values.deconstruct_keys(keys)
417
+
418
+ def to_source_trace() = __trace__&.to_h
419
+
420
+ def inspect
421
+ "#<#{self.class}:0x#{vm_object_id.rjust(16, "0")} config_name=\"#{config_name}\" env_prefix=\"#{env_prefix}\" " \
422
+ "values=#{values.inspect}>"
423
+ end
424
+
425
+ def pretty_print(q)
426
+ q.object_group self do
427
+ q.nest(1) do
428
+ q.breakable
429
+ q.text "config_name=#{config_name.inspect}"
430
+ q.breakable
431
+ q.text "env_prefix=#{env_prefix.inspect}"
432
+ q.breakable
433
+ q.text "values:"
434
+ q.pp __trace__
435
+ end
436
+ end
437
+ end
438
+
439
+ def as_env
440
+ Env.from_hash(to_h, prefix: env_prefix)
441
+ end
442
+
443
+ private
444
+
445
+ attr_reader :values, :__trace__
446
+
447
+ def validate_required_attributes!
448
+ self.class.required_attributes.select do |name|
449
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
450
+ val.nil? || (val.is_a?(String) && val.empty?)
451
+ end.then do |missing|
452
+ next if missing.empty?
453
+ raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
454
+ end
455
+ end
456
+
457
+ def write_config_attr(key, val)
458
+ key = key.to_sym
459
+ return unless self.class.config_attributes.include?(key)
460
+
461
+ val = __type_caster__.coerce(key, val)
462
+ public_send(:"#{key}=", val)
463
+ end
464
+
465
+ def raise_validation_error(msg)
466
+ raise ValidationError, msg
467
+ end
468
+
469
+ def __type_caster__
470
+ self.class.type_caster
471
+ end
472
+ end
473
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Adds ability to generate anonymous (class-less) config dynamicly
5
+ # (like Rails.application.config_for but using more data sources).
6
+ module DynamicConfig
7
+ module ClassMethods
8
+ # Load config as Hash by any name
9
+ #
10
+ # Example:
11
+ #
12
+ # my_config = Anyway::Config.for(:my_app)
13
+ # # will load data from config/my_app.yml, secrets.my_app, ENV["MY_APP_*"]
14
+ #
15
+ def for(name, auto_cast: true, **options)
16
+ config = allocate
17
+ options[:env_prefix] ||= name.to_s.upcase
18
+ options[:config_path] ||= config.resolve_config_path(name, options[:env_prefix])
19
+
20
+ raw_config = config.load_from_sources(new_empty_config, name:, **options)
21
+ return raw_config unless auto_cast
22
+
23
+ AutoCast.call(raw_config)
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "anyway/ext/hash"
5
+
6
+ using Anyway::Ext::Hash
7
+
8
+ module Anyway
9
+ class EJSONParser
10
+ attr_reader :bin_path
11
+
12
+ def initialize(bin_path = "ejson")
13
+ @bin_path = bin_path
14
+ end
15
+
16
+ def call(file_path)
17
+ return unless File.exist?(file_path)
18
+
19
+ raw_content = nil
20
+
21
+ stdout, stderr, status = Open3.capture3("#{bin_path} decrypt #{file_path}")
22
+
23
+ if status.success?
24
+ raw_content = JSON.parse(stdout.chomp)
25
+ else
26
+ Kernel.warn "Failed to decrypt #{file_path}: #{stderr}"
27
+ end
28
+
29
+ return unless raw_content
30
+
31
+ raw_content.deep_transform_keys do |key|
32
+ if key[0] == "_"
33
+ key[1..]
34
+ else
35
+ key
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/anyway/env.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Parses environment variables and provides
5
+ # method-like access
6
+ class Env
7
+ using RubyNext
8
+ using Anyway::Ext::DeepDup
9
+ using Anyway::Ext::Hash
10
+
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo:)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
27
+ include Tracing
28
+
29
+ attr_reader :data, :traces, :type_cast, :env_container
30
+
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
32
+ @type_cast = type_cast
33
+ @data = {}
34
+ @traces = {}
35
+ @env_container = env_container
36
+ end
37
+
38
+ def clear
39
+ data.clear
40
+ traces.clear
41
+ end
42
+
43
+ def fetch(prefix)
44
+ return data[prefix].deep_dup if data.key?(prefix)
45
+
46
+ Tracing.capture do
47
+ data[prefix] = parse_env(prefix)
48
+ end.then do |trace|
49
+ traces[prefix] = trace
50
+ end
51
+
52
+ data[prefix].deep_dup
53
+ end
54
+
55
+ def fetch_with_trace(prefix)
56
+ [fetch(prefix), traces[prefix]]
57
+ end
58
+
59
+ private
60
+
61
+ def parse_env(prefix)
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
64
+ next unless key.start_with?(match_prefix)
65
+
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
67
+
68
+ paths = path.split("__")
69
+ trace!(:env, *paths, key:) { data.bury(type_cast.call(val), *paths) }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Ext
5
+ # Extend Object through refinements
6
+ module DeepDup
7
+ refine ::Hash do
8
+ # Based on ActiveSupport http://api.rubyonrails.org/classes/Hash.html#method-i-deep_dup
9
+ def deep_dup
10
+ each_with_object(dup) do |(key, value), hash|
11
+ hash[key] = if value.is_a?(::Hash) || value.is_a?(::Array)
12
+ value.deep_dup
13
+ else
14
+ value
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ refine ::Array do
21
+ # From ActiveSupport http://api.rubyonrails.org/classes/Array.html#method-i-deep_dup
22
+ def deep_dup
23
+ map do |value|
24
+ if value.is_a?(::Hash) || value.is_a?(::Array)
25
+ value.deep_dup
26
+ else
27
+ value
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ refine ::Object do
34
+ def deep_dup
35
+ dup
36
+ end
37
+ end
38
+
39
+ refine ::Module do
40
+ def deep_dup
41
+ self
42
+ end
43
+ end
44
+
45
+ using self
46
+ end
47
+ end
48
+ end