class Safe
A safe format for converting objects to strings.
Handles issues like circular references and encoding errors.
Definitions
def initialize(format: ::JSON, limit: 8, encoding: ::Encoding::UTF_8)
Create a new safe format.
Signature
-
parameter
format
JSON
The format to use for serialization.
-
parameter
limit
Integer
The maximum depth to recurse into objects.
-
parameter
encoding
Encoding
The encoding to use for strings.
Implementation
def initialize(format: ::JSON, limit: 8, encoding: ::Encoding::UTF_8)
@format = format
@limit = limit
@encoding = encoding
end
def dump(object)
Dump the given object to a string.
Signature
-
parameter
object
Object
The object to dump.
-
returns
String
The dumped object.
Implementation
def dump(object)
@format.dump(object, @limit)
rescue SystemStackError, StandardError => error
@format.dump(safe_dump(object, error))
end
def filter_backtrace(error)
Filter the backtrace to remove duplicate frames and reduce verbosity.
Signature
-
parameter
error
Exception
The exception to filter.
-
returns
Array(String)
The filtered backtrace.
Implementation
def filter_backtrace(error)
frames = error.backtrace
filtered = {}
filtered_count = nil
skipped = nil
frames = frames.filter_map do |frame|
if filtered[frame]
if filtered_count == nil
filtered_count = 1
skipped = frame.dup
else
filtered_count += 1
nil
end
else
if skipped
if filtered_count > 1
skipped.replace("[... #{filtered_count} frames skipped ...]")
end
filtered_count = nil
skipped = nil
end
filtered[frame] = true
frame
end
end
if skipped && filtered_count > 1
skipped.replace("[... #{filtered_count} frames skipped ...]")
end
return frames
end
def safe_dump(object, error)
Dump the given object to a string, replacing it with a safe representation if there is an error.
This is a slow path so we try to avoid it.
Signature
-
parameter
object
Object
The object to dump.
-
parameter
error
Exception
The error that occurred while dumping the object.
-
returns
Hash
The dumped (truncated) object including error details.
Implementation
def safe_dump(object, error)
object = safe_dump_recurse(object)
object[:truncated] = true
object[:error] = {
class: safe_dump_recurse(error.class.name),
message: safe_dump_recurse(error.message),
backtrace: safe_dump_recurse(filter_backtrace(error)),
}
return object
end
def replacement_for(object)
Replace the given object with a safe truncated representation.
Signature
-
parameter
object
Object
The object to replace.
-
returns
String
The replacement string.
Implementation
def replacement_for(object)
case object
when Array
"[...]"
when Hash
"{...}"
else
"..."
end
end
def default_objects
Create a new hash with identity comparison.
Implementation
def default_objects
Hash.new.compare_by_identity
end
def safe_dump_recurse(object, limit = @limit, objects = default_objects)
This will recursively generate a safe version of the object. Nested hashes and arrays will be transformed recursively. Strings will be encoded with the given encoding. Primitive values will be returned as-is. Other values will be converted using as_json
if available, otherwise to_s
.
Signature
-
parameter
object
Object
The object to dump.
-
parameter
limit
Integer
The maximum depth to recurse into objects.
-
parameter
objects
Hash
The objects that have already been visited.
-
returns
Object
The dumped object as a primitive representation.
Implementation
def safe_dump_recurse(object, limit = @limit, objects = default_objects)
if limit <= 0 || objects[object]
return replacement_for(object)
end
case object
when Hash
objects[object] = true
object.to_h do |key, value|
[
String(key).encode(@encoding, invalid: :replace, undef: :replace),
safe_dump_recurse(value, limit - 1, objects)
]
end
when Array
objects[object] = true
object.map do |value|
safe_dump_recurse(value, limit - 1, objects)
end
when String
object.encode(@encoding, invalid: :replace, undef: :replace)
when Numeric, TrueClass, FalseClass, NilClass
object
else
objects[object] = true
# We could do something like this but the chance `as_json` will blow up.
# We'd need to be extremely careful about it.
# if object.respond_to?(:as_json)
# safe_dump_recurse(object.as_json, limit - 1, objects)
# else
safe_dump_recurse(object.to_s, limit - 1, objects)
end
end