1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
|
# frozen_string_literal: true
require "set"
module Lumberjack
class Formatter
# Dereference arrays and hashes and recursively call formatters on each element.
# This formatter provides deep traversal of nested data structures, applying
# formatting to all contained elements while handling circular references safely.
#
# The StructuredFormatter is essential for formatting complex nested objects
# like configuration hashes, API responses, or any hierarchical data structures
# that need consistent formatting throughout their entire structure.
class StructuredFormatter
FormatterRegistry.add(:structured, self)
# Exception raised when a circular reference is detected during traversal.
# This prevents infinite recursion when formatting objects that reference themselves.
class RecusiveReferenceError < StandardError
end
# @param formatter [Formatter, nil] The formatter to call on each element
# in the structure. If nil, elements are returned unchanged.
def initialize(formatter = nil)
@formatter = formatter
end
# Format a structured object by recursively processing all nested elements.
#
# @param obj [Object] The object to format. Arrays and hashes are traversed
# recursively, while other objects are passed to the configured formatter.
# @return [Object] The formatted structure with all nested elements processed.
def call(obj)
call_with_references(obj, Set.new)
end
private
def call_with_references(obj, references)
if obj.is_a?(Hash)
with_object_reference(obj, references) do
hash = {}
obj.each do |name, value|
value = call_with_references(value, references)
hash[name.to_s] = value unless value.is_a?(RecusiveReferenceError)
end
hash
end
elsif obj.is_a?(Enumerable) && obj.respond_to?(:size) && obj.size != Float::INFINITY
with_object_reference(obj, references) do
array = []
obj.each do |value|
value = call_with_references(value, references)
array << value unless value.is_a?(RecusiveReferenceError)
end
array
end
elsif @formatter
@formatter.format(obj)
else
obj
end
end
def with_object_reference(obj, references)
if obj.is_a?(Enumerable)
return RecusiveReferenceError.new if references.include?(obj.object_id)
references << obj.object_id
begin
yield
ensure
references.delete(obj.object_id)
end
else
yield
end
end
end
end
end
|