bugsnag 4.2.1 → 6.27.1

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 (106) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +12 -0
  3. data/CHANGELOG.md +814 -0
  4. data/README.md +21 -25
  5. data/VERSION +1 -1
  6. data/bugsnag.gemspec +19 -8
  7. data/lib/bugsnag/breadcrumb_type.rb +14 -0
  8. data/lib/bugsnag/breadcrumbs/breadcrumb.rb +109 -0
  9. data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +13 -0
  10. data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +48 -0
  11. data/lib/bugsnag/breadcrumbs/validator.rb +29 -0
  12. data/lib/bugsnag/cleaner.rb +170 -59
  13. data/lib/bugsnag/code_extractor.rb +137 -0
  14. data/lib/bugsnag/configuration.rb +670 -45
  15. data/lib/bugsnag/delivery/synchronous.rb +31 -14
  16. data/lib/bugsnag/delivery/thread_queue.rb +23 -6
  17. data/lib/bugsnag/delivery.rb +13 -0
  18. data/lib/bugsnag/endpoint_configuration.rb +11 -0
  19. data/lib/bugsnag/endpoint_validator.rb +80 -0
  20. data/lib/bugsnag/error.rb +25 -0
  21. data/lib/bugsnag/event.rb +5 -0
  22. data/lib/bugsnag/feature_flag.rb +74 -0
  23. data/lib/bugsnag/helpers.rb +121 -25
  24. data/lib/bugsnag/integrations/delayed_job.rb +51 -0
  25. data/lib/bugsnag/integrations/mailman.rb +43 -0
  26. data/lib/bugsnag/integrations/mongo.rb +133 -0
  27. data/lib/bugsnag/integrations/que.rb +53 -0
  28. data/lib/bugsnag/integrations/rack.rb +83 -0
  29. data/lib/bugsnag/integrations/rails/active_job.rb +100 -0
  30. data/lib/bugsnag/{rails → integrations/rails}/active_record_rescue.rb +10 -1
  31. data/lib/bugsnag/{rails → integrations/rails}/controller_methods.rb +1 -9
  32. data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +115 -0
  33. data/lib/bugsnag/integrations/railtie.rb +153 -0
  34. data/lib/bugsnag/integrations/rake.rb +74 -0
  35. data/lib/bugsnag/integrations/resque.rb +94 -0
  36. data/lib/bugsnag/integrations/shoryuken.rb +50 -0
  37. data/lib/bugsnag/integrations/sidekiq.rb +68 -0
  38. data/lib/bugsnag/meta_data.rb +1 -0
  39. data/lib/bugsnag/middleware/active_job.rb +18 -0
  40. data/lib/bugsnag/middleware/breadcrumbs.rb +21 -0
  41. data/lib/bugsnag/middleware/callbacks.rb +6 -8
  42. data/lib/bugsnag/middleware/classify_error.rb +50 -0
  43. data/lib/bugsnag/middleware/clearance_user.rb +33 -0
  44. data/lib/bugsnag/middleware/delayed_job.rb +93 -0
  45. data/lib/bugsnag/middleware/discard_error_class.rb +30 -0
  46. data/lib/bugsnag/middleware/exception_meta_data.rb +42 -0
  47. data/lib/bugsnag/middleware/ignore_error_class.rb +26 -0
  48. data/lib/bugsnag/middleware/mailman.rb +6 -4
  49. data/lib/bugsnag/middleware/rack_request.rb +126 -30
  50. data/lib/bugsnag/middleware/rails3_request.rb +15 -17
  51. data/lib/bugsnag/middleware/rake.rb +7 -5
  52. data/lib/bugsnag/middleware/session_data.rb +25 -0
  53. data/lib/bugsnag/middleware/sidekiq.rb +9 -4
  54. data/lib/bugsnag/middleware/suggestion_data.rb +34 -0
  55. data/lib/bugsnag/middleware/warden_user.rb +11 -6
  56. data/lib/bugsnag/middleware_stack.rb +62 -9
  57. data/lib/bugsnag/on_error_callbacks.rb +33 -0
  58. data/lib/bugsnag/report.rb +516 -0
  59. data/lib/bugsnag/session_tracker.rb +182 -0
  60. data/lib/bugsnag/stacktrace.rb +82 -0
  61. data/lib/bugsnag/tasks/bugsnag.rake +2 -70
  62. data/lib/bugsnag/utility/circular_buffer.rb +62 -0
  63. data/lib/bugsnag/utility/duplicator.rb +124 -0
  64. data/lib/bugsnag/utility/feature_data_store.rb +41 -0
  65. data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
  66. data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
  67. data/lib/bugsnag.rb +528 -80
  68. metadata +61 -123
  69. data/.document +0 -5
  70. data/.gitignore +0 -52
  71. data/.rspec +0 -3
  72. data/.travis.yml +0 -14
  73. data/CONTRIBUTING.md +0 -47
  74. data/Gemfile +0 -2
  75. data/Rakefile +0 -29
  76. data/lib/bugsnag/capistrano.rb +0 -7
  77. data/lib/bugsnag/capistrano2.rb +0 -32
  78. data/lib/bugsnag/delay/resque.rb +0 -21
  79. data/lib/bugsnag/delayed_job.rb +0 -57
  80. data/lib/bugsnag/deploy.rb +0 -34
  81. data/lib/bugsnag/mailman.rb +0 -28
  82. data/lib/bugsnag/middleware/rails2_request.rb +0 -52
  83. data/lib/bugsnag/notification.rb +0 -459
  84. data/lib/bugsnag/rack.rb +0 -53
  85. data/lib/bugsnag/rails/action_controller_rescue.rb +0 -62
  86. data/lib/bugsnag/rails.rb +0 -66
  87. data/lib/bugsnag/railtie.rb +0 -80
  88. data/lib/bugsnag/rake.rb +0 -25
  89. data/lib/bugsnag/resque.rb +0 -40
  90. data/lib/bugsnag/sidekiq.rb +0 -42
  91. data/lib/bugsnag/tasks/bugsnag.cap +0 -48
  92. data/rails/init.rb +0 -7
  93. data/spec/cleaner_spec.rb +0 -138
  94. data/spec/code_spec.rb +0 -86
  95. data/spec/fixtures/crashes/end_of_file.rb +0 -9
  96. data/spec/fixtures/crashes/short_file.rb +0 -1
  97. data/spec/fixtures/crashes/start_of_file.rb +0 -9
  98. data/spec/fixtures/middleware/internal_info_setter.rb +0 -11
  99. data/spec/fixtures/middleware/public_info_setter.rb +0 -11
  100. data/spec/fixtures/tasks/Rakefile +0 -15
  101. data/spec/helper_spec.rb +0 -163
  102. data/spec/integration_spec.rb +0 -132
  103. data/spec/middleware_spec.rb +0 -181
  104. data/spec/notification_spec.rb +0 -877
  105. data/spec/rack_spec.rb +0 -56
  106. data/spec/spec_helper.rb +0 -53
@@ -0,0 +1,82 @@
1
+ require_relative 'code_extractor'
2
+
3
+ module Bugsnag
4
+ module Stacktrace
5
+ # e.g. "org/jruby/RubyKernel.java:1264:in `catch'"
6
+ BACKTRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in [`']([^']+)')?$/
7
+
8
+ # e.g. "org.jruby.Ruby.runScript(Ruby.java:807)"
9
+ JAVA_BACKTRACE_REGEX = /^(.*)\((.*)(?::([0-9]+))?\)$/
10
+
11
+ ##
12
+ # Process a backtrace and the configuration into a parsed stacktrace.
13
+ #
14
+ # @param backtrace [Array, nil] If nil, 'caller' will be used instead
15
+ # @param configuration [Configuration]
16
+ # @return [Array]
17
+ def self.process(backtrace, configuration)
18
+ code_extractor = CodeExtractor.new(configuration)
19
+
20
+ backtrace = caller if !backtrace || backtrace.empty?
21
+
22
+ processed_backtrace = backtrace.map do |trace|
23
+ # Parse the stacktrace line
24
+ if trace.match(BACKTRACE_LINE_REGEX)
25
+ file, line_str, method = [$1, $2, $3]
26
+ elsif trace.match(JAVA_BACKTRACE_REGEX)
27
+ method, file, line_str = [$1, $2, $3]
28
+ end
29
+
30
+ next if file.nil?
31
+
32
+ # Expand relative paths
33
+ file = File.realpath(file) rescue file
34
+
35
+ # Generate the stacktrace line hash
36
+ trace_hash = { lineNumber: line_str.to_i }
37
+
38
+ # Save a copy of the file path as we're about to modify it but need the
39
+ # raw version when extracting code (otherwise we can't open the file)
40
+ raw_file_path = file.dup
41
+
42
+ # Clean up the file path in the stacktrace
43
+ if defined?(configuration.project_root) && configuration.project_root.to_s != ''
44
+ trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s)
45
+ file.sub!(/#{configuration.project_root}\//, "")
46
+ trace_hash.delete(:inProject) if vendor_path?(configuration, file)
47
+ end
48
+
49
+ # Strip common gem path prefixes
50
+ if defined?(Gem)
51
+ Gem.path.each do |path|
52
+ file.sub!("#{path}/", "")
53
+ end
54
+ end
55
+
56
+ trace_hash[:file] = file
57
+
58
+ # Add a method if we have it
59
+ trace_hash[:method] = method if method && (method =~ /^__bind/).nil?
60
+
61
+ # If we're going to send code then record the raw file path and the
62
+ # trace_hash, so we can extract from it later
63
+ code_extractor.add_file(raw_file_path, trace_hash) if configuration.send_code
64
+
65
+ trace_hash
66
+ end.compact
67
+
68
+ code_extractor.extract! if configuration.send_code
69
+
70
+ processed_backtrace
71
+ end
72
+
73
+ # @api private
74
+ def self.vendor_path?(configuration, file_path)
75
+ return true if configuration.vendor_path && file_path.match(configuration.vendor_path)
76
+
77
+ configuration.vendor_paths.any? do |vendor_path|
78
+ file_path.start_with?("#{vendor_path.sub(/\/$/, '')}/")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,82 +1,14 @@
1
1
  require "bugsnag"
2
2
 
3
3
  namespace :bugsnag do
4
- desc "Notify Bugsnag of a new deploy."
5
- task :deploy do
6
- api_key = ENV["BUGSNAG_API_KEY"]
7
- release_stage = ENV["BUGSNAG_RELEASE_STAGE"]
8
- app_version = ENV["BUGSNAG_APP_VERSION"]
9
- revision = ENV["BUGSNAG_REVISION"]
10
- repository = ENV["BUGSNAG_REPOSITORY"]
11
- branch = ENV["BUGSNAG_BRANCH"]
12
-
13
- Rake::Task["load"].invoke unless api_key
14
-
15
- Bugsnag::Deploy.notify({
16
- :api_key => api_key,
17
- :release_stage => release_stage,
18
- :app_version => app_version,
19
- :revision => revision,
20
- :repository => repository,
21
- :branch => branch
22
- })
23
- end
24
-
25
4
  desc "Send a test exception to Bugsnag."
26
5
  task :test_exception => :load do
27
6
  begin
28
7
  raise RuntimeError.new("Bugsnag test exception")
29
8
  rescue => e
30
- Bugsnag.notify(e, {:context => "rake#test_exception"})
31
- end
32
- end
33
-
34
- desc "Show the bugsnag middleware stack"
35
- task :middleware => :load do
36
- Bugsnag.configuration.middleware.each {|m| puts m.to_s}
37
- end
38
-
39
- namespace :heroku do
40
- desc "Add a heroku deploy hook to notify Bugsnag of deploys"
41
- task :add_deploy_hook => :load do
42
- # Wrapper to run command safely even in bundler
43
- run_command = lambda { |command|
44
- defined?(Bundler.with_clean_env) ? Bundler.with_clean_env { `#{command}` } : `#{command}`
45
- }
46
-
47
- # Fetch heroku config settings
48
- config_command = "heroku config --shell"
49
- config_command += " --app #{ENV["HEROKU_APP"]}" if ENV["HEROKU_APP"]
50
- heroku_env = run_command.call(config_command).split(/[\n\r]/).each_with_object({}) do |c, obj|
51
- k,v = c.split("=")
52
- obj[k] = (v.nil? || v.strip.empty?) ? nil : v
53
- end
54
-
55
- # Check for Bugsnag API key (required)
56
- api_key = heroku_env["BUGSNAG_API_KEY"] || Bugsnag.configuration.api_key || ENV["BUGSNAG_API_KEY"]
57
- unless api_key
58
- puts "Error: No API key found, have you run 'heroku config:set BUGSNAG_API_KEY=your-api-key'?"
59
- next
9
+ Bugsnag.notify(e) do |report|
10
+ report.automatic_context = "rake#test_exception"
60
11
  end
61
-
62
- # Build the request, making use of deploy hook variables
63
- # (https://devcenter.heroku.com/articles/deploy-hooks#customizing-messages)
64
- params = {
65
- :apiKey => api_key,
66
- :branch => "master",
67
- :revision => "{{head_long}}",
68
- :releaseStage => heroku_env["RAILS_ENV"] || ENV["RAILS_ENV"] || "production"
69
- }
70
- repo = `git config --get remote.origin.url`.strip
71
- params[:repository] = repo unless repo.empty?
72
-
73
- # Add the hook
74
- url = "https://notify.bugsnag.com/deploy?" + params.map {|k,v| "#{k}=#{v}"}.join("&")
75
- command = "heroku addons:add deployhooks:http --url=\"#{url}\""
76
- command += " --app #{ENV["HEROKU_APP"]}" if ENV["HEROKU_APP"]
77
-
78
- puts "$ #{command}"
79
- run_command.call(command)
80
12
  end
81
13
  end
82
14
  end
@@ -0,0 +1,62 @@
1
+ module Bugsnag::Utility
2
+ ##
3
+ # A container class with a maximum size, that removes oldest items as required.
4
+ #
5
+ # @api private
6
+ class CircularBuffer
7
+ include Enumerable
8
+
9
+ # @return [Integer] the current maximum allowable number of items
10
+ attr_reader :max_items
11
+
12
+ ##
13
+ # @param max_items [Integer] the initial maximum number of items
14
+ def initialize(max_items = 25)
15
+ @max_items = max_items
16
+ @buffer = []
17
+ end
18
+
19
+ ##
20
+ # Adds an item to the circular buffer
21
+ #
22
+ # If this causes the buffer to exceed its maximum items, the oldest item will be removed
23
+ #
24
+ # @param item [Object] the item to add to the buffer
25
+ # @return [self] returns itself to allow method chaining
26
+ def <<(item)
27
+ @buffer << item
28
+ trim_buffer
29
+ self
30
+ end
31
+
32
+ ##
33
+ # Iterates over the buffer
34
+ #
35
+ # @yield [Object] sequentially gives stored items to the block
36
+ def each(&block)
37
+ @buffer.each(&block)
38
+ end
39
+
40
+ ##
41
+ # Sets the maximum allowable number of items
42
+ #
43
+ # If the current number of items exceeds the new maximum, oldest items will be removed
44
+ # until this is no longer the case
45
+ #
46
+ # @param new_max_items [Integer] the new allowed item maximum
47
+ def max_items=(new_max_items)
48
+ @max_items = new_max_items
49
+ trim_buffer
50
+ end
51
+
52
+ private
53
+
54
+ ##
55
+ # Trims the buffer down to the current maximum allowable item number
56
+ def trim_buffer
57
+ trim_size = @buffer.size - @max_items
58
+ trim_size = 0 if trim_size < 0
59
+ @buffer.shift(trim_size)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,124 @@
1
+ module Bugsnag::Utility
2
+ # @api private
3
+ class Duplicator
4
+ class << self
5
+ ##
6
+ # Duplicate (deep clone) the given object
7
+ #
8
+ # @param object [Object]
9
+ # @param seen_objects [Hash<String, Object>]
10
+ # @return [Object]
11
+ def duplicate(object, seen_objects = {})
12
+ case object
13
+ # return immutable & non-duplicatable objects as-is
14
+ when Symbol, Numeric, Method, TrueClass, FalseClass, NilClass
15
+ object
16
+ when Array
17
+ duplicate_array(object, seen_objects)
18
+ when Hash
19
+ duplicate_hash(object, seen_objects)
20
+ when Range
21
+ duplicate_range(object, seen_objects)
22
+ when Struct
23
+ duplicate_struct(object, seen_objects)
24
+ else
25
+ duplicate_generic_object(object, seen_objects)
26
+ end
27
+ rescue StandardError
28
+ object
29
+ end
30
+
31
+ private
32
+
33
+ def duplicate_array(array, seen_objects)
34
+ id = array.object_id
35
+
36
+ return seen_objects[id] if seen_objects.key?(id)
37
+
38
+ copy = array.dup
39
+ seen_objects[id] = copy
40
+
41
+ copy.map! do |value|
42
+ duplicate(value, seen_objects)
43
+ end
44
+
45
+ copy
46
+ end
47
+
48
+ def duplicate_hash(hash, seen_objects)
49
+ id = hash.object_id
50
+
51
+ return seen_objects[id] if seen_objects.key?(id)
52
+
53
+ copy = {}
54
+ seen_objects[id] = copy
55
+
56
+ hash.each do |key, value|
57
+ copy[duplicate(key, seen_objects)] = duplicate(value, seen_objects)
58
+ end
59
+
60
+ copy
61
+ end
62
+
63
+ ##
64
+ # Ranges are immutable but the values they contain may not be
65
+ #
66
+ # For example, a range of "a".."z" can be mutated: range.first.upcase!
67
+ def duplicate_range(range, seen_objects)
68
+ id = range.object_id
69
+
70
+ return seen_objects[id] if seen_objects.key?(id)
71
+
72
+ begin
73
+ copy = range.class.new(
74
+ duplicate(range.first, seen_objects),
75
+ duplicate(range.last, seen_objects),
76
+ range.exclude_end?
77
+ )
78
+ rescue StandardError
79
+ copy = range.dup
80
+ end
81
+
82
+ seen_objects[id] = copy
83
+ end
84
+
85
+ def duplicate_struct(struct, seen_objects)
86
+ id = struct.object_id
87
+
88
+ return seen_objects[id] if seen_objects.key?(id)
89
+
90
+ copy = struct.dup
91
+ seen_objects[id] = copy
92
+
93
+ struct.each_pair do |attribute, value|
94
+ begin
95
+ copy.send("#{attribute}=", duplicate(value, seen_objects))
96
+ rescue StandardError # rubocop:todo Lint/SuppressedException
97
+ end
98
+ end
99
+
100
+ copy
101
+ end
102
+
103
+ def duplicate_generic_object(object, seen_objects)
104
+ id = object.object_id
105
+
106
+ return seen_objects[id] if seen_objects.key?(id)
107
+
108
+ copy = object.dup
109
+ seen_objects[id] = copy
110
+
111
+ begin
112
+ copy.instance_variables.each do |variable|
113
+ value = copy.instance_variable_get(variable)
114
+
115
+ copy.instance_variable_set(variable, duplicate(value, seen_objects))
116
+ end
117
+ rescue StandardError # rubocop:todo Lint/SuppressedException
118
+ end
119
+
120
+ copy
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,41 @@
1
+ module Bugsnag::Utility
2
+ # @abstract Requires a #feature_flag_delegate method returning a
3
+ # {Bugsnag::Utility::FeatureFlagDelegate}
4
+ module FeatureDataStore
5
+ # Add a feature flag with the given name & variant
6
+ #
7
+ # @param name [String]
8
+ # @param variant [String, nil]
9
+ # @return [void]
10
+ def add_feature_flag(name, variant = nil)
11
+ feature_flag_delegate.add(name, variant)
12
+ end
13
+
14
+ # Merge the given array of FeatureFlag instances into the stored feature
15
+ # flags
16
+ #
17
+ # New flags will be appended to the array. Flags with the same name will be
18
+ # overwritten, but their position in the array will not change
19
+ #
20
+ # @param feature_flags [Array<Bugsnag::FeatureFlag>]
21
+ # @return [void]
22
+ def add_feature_flags(feature_flags)
23
+ feature_flag_delegate.merge(feature_flags)
24
+ end
25
+
26
+ # Remove the stored flag with the given name
27
+ #
28
+ # @param name [String]
29
+ # @return [void]
30
+ def clear_feature_flag(name)
31
+ feature_flag_delegate.remove(name)
32
+ end
33
+
34
+ # Remove all the stored flags
35
+ #
36
+ # @return [void]
37
+ def clear_feature_flags
38
+ feature_flag_delegate.clear
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,89 @@
1
+ module Bugsnag::Utility
2
+ # @api private
3
+ class FeatureFlagDelegate
4
+ def initialize
5
+ # feature flags are stored internally in a hash of "name" => <FeatureFlag>
6
+ # we don't use a Set because new feature flags should overwrite old ones
7
+ # that share a name, but FeatureFlag equality also uses the variant
8
+ @storage = {}
9
+ end
10
+
11
+ def initialize_dup(original)
12
+ super
13
+
14
+ # copy the internal storage when 'dup' is called
15
+ @storage = @storage.dup
16
+ end
17
+
18
+ # Add a feature flag with the given name & variant
19
+ #
20
+ # @param name [String]
21
+ # @param variant [String, nil]
22
+ # @return [void]
23
+ def add(name, variant)
24
+ flag = Bugsnag::FeatureFlag.new(name, variant)
25
+
26
+ return unless flag.valid?
27
+
28
+ @storage[flag.name] = flag
29
+ end
30
+
31
+ # Merge the given array of FeatureFlag instances into the stored feature
32
+ # flags
33
+ #
34
+ # New flags will be appended to the array. Flags with the same name will be
35
+ # overwritten, but their position in the array will not change
36
+ #
37
+ # @param feature_flags [Array<Bugsnag::FeatureFlag>]
38
+ # @return [void]
39
+ def merge(feature_flags)
40
+ feature_flags.each do |flag|
41
+ next unless flag.is_a?(Bugsnag::FeatureFlag)
42
+ next unless flag.valid?
43
+
44
+ @storage[flag.name] = flag
45
+ end
46
+ end
47
+
48
+ # Remove the stored flag with the given name
49
+ #
50
+ # @param name [String]
51
+ # @return [void]
52
+ def remove(name)
53
+ @storage.delete(name)
54
+ end
55
+
56
+ # Remove all the stored flags
57
+ #
58
+ # @return [void]
59
+ def clear
60
+ @storage.clear
61
+ end
62
+
63
+ # Get an array of FeatureFlag instances
64
+ #
65
+ # @example
66
+ # [
67
+ # <#Bugsnag::FeatureFlag>,
68
+ # <#Bugsnag::FeatureFlag>,
69
+ # ]
70
+ #
71
+ # @return [Array<Bugsnag::FeatureFlag>]
72
+ def to_a
73
+ @storage.values
74
+ end
75
+
76
+ # Get the feature flags in their JSON representation
77
+ #
78
+ # @example
79
+ # [
80
+ # { "featureFlag" => "name", "variant" => "variant" },
81
+ # { "featureFlag" => "another name" },
82
+ # ]
83
+ #
84
+ # @return [Array<Hash{String => String}>]
85
+ def as_json
86
+ to_a.map(&:to_h)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,102 @@
1
+ module Bugsnag::Utility
2
+ # @api private
3
+ class MetadataDelegate
4
+ # nil is a valid metadata value, so we need a sentinel object so we can tell
5
+ # if the value parameter has been provided
6
+ NOT_PROVIDED = Object.new
7
+
8
+ ##
9
+ # Add values to metadata
10
+ #
11
+ # @overload add_metadata(metadata, section, data)
12
+ # Merges data into the given section of metadata
13
+ # @param metadata [Hash] The metadata hash to operate on
14
+ # @param section [String, Symbol]
15
+ # @param data [Hash]
16
+ #
17
+ # @overload add_metadata(metadata, section, key, value)
18
+ # Sets key to value in the given section of metadata. If the value is nil
19
+ # the key will be deleted
20
+ # @param metadata [Hash] The metadata hash to operate on
21
+ # @param section [String, Symbol]
22
+ # @param key [String, Symbol]
23
+ # @param value
24
+ #
25
+ # @return [void]
26
+ def add_metadata(metadata, section, key_or_data, value = NOT_PROVIDED)
27
+ case value
28
+ when NOT_PROVIDED
29
+ merge_metadata(metadata, section, key_or_data)
30
+ when nil
31
+ clear_metadata(metadata, section, key_or_data)
32
+ else
33
+ overwrite_metadata(metadata, section, key_or_data, value)
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Clear values from metadata
39
+ #
40
+ # @overload clear_metadata(metadata, section)
41
+ # Clears the given section of metadata
42
+ # @param metadata [Hash] The metadata hash to operate on
43
+ # @param section [String, Symbol]
44
+ #
45
+ # @overload clear_metadata(metadata, section, key)
46
+ # Clears the key in the given section of metadata
47
+ # @param metadata [Hash] The metadata hash to operate on
48
+ # @param section [String, Symbol]
49
+ # @param key [String, Symbol]
50
+ #
51
+ # @return [void]
52
+ def clear_metadata(metadata, section, key = nil)
53
+ if key.nil?
54
+ metadata.delete(section)
55
+ elsif metadata[section]
56
+ metadata[section].delete(key)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ ##
63
+ # Merge new metadata into the existing metadata
64
+ #
65
+ # Any keys with a 'nil' value in the new metadata will be deleted from the
66
+ # existing metadata
67
+ #
68
+ # @param existing_metadata [Hash]
69
+ # @param section [String, Symbol]
70
+ # @param new_metadata [Hash]
71
+ # @return [void]
72
+ def merge_metadata(existing_metadata, section, new_metadata)
73
+ return unless new_metadata.is_a?(Hash)
74
+
75
+ existing_metadata[section] ||= {}
76
+ data = existing_metadata[section]
77
+
78
+ new_metadata.each do |key, value|
79
+ if value.nil?
80
+ data.delete(key)
81
+ else
82
+ data[key] = value
83
+ end
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Overwrite the value in metadata's section & key
89
+ #
90
+ # @param metadata [Hash]
91
+ # @param section [String, Symbol]
92
+ # @param key [String, Symbol]
93
+ # @param value
94
+ # @return [void]
95
+ def overwrite_metadata(metadata, section, key, value)
96
+ return unless key.is_a?(String) || key.is_a?(Symbol)
97
+
98
+ metadata[section] ||= {}
99
+ metadata[section][key] = value
100
+ end
101
+ end
102
+ end