mirror of
https://github.com/meineerde/rackstash.git
synced 2025-10-17 14:01:01 +00:00
220 lines
6.6 KiB
Ruby
220 lines
6.6 KiB
Ruby
# frozen_string_literal: true
|
|
#
|
|
# Copyright 2017 - 2020 Holger Just
|
|
#
|
|
# This software may be modified and distributed under the terms
|
|
# of the MIT license. See the LICENSE.txt file for details.
|
|
|
|
require 'bigdecimal'
|
|
require 'pathname'
|
|
require 'uri'
|
|
|
|
require 'concurrent'
|
|
|
|
require 'rackstash/utils'
|
|
|
|
module Rackstash
|
|
module Fields
|
|
class AbstractCollection
|
|
include Rackstash::Utils
|
|
|
|
# Equality - Two collections are equal if they are of exactly the same
|
|
# class and contain the same raw data according to `Object#==`.
|
|
#
|
|
# @return [Boolean] `true` if `other` is an object of the same class and
|
|
# contains the same raw data as this object.
|
|
def ==(other)
|
|
self.class == other.class && raw == other.raw
|
|
end
|
|
alias eql? ==
|
|
|
|
# Prevents further modifications to `self`. A `RuntimeError` will be
|
|
# raised if modification is attempted. There is no way to unfreeze a
|
|
# frozen object.
|
|
#
|
|
# @return [self]
|
|
def freeze
|
|
raw.freeze
|
|
super
|
|
end
|
|
|
|
# Compute a hash-code for this collection.
|
|
#
|
|
# Two collections with the same raw content will have the same hash code
|
|
# (and will compare using {#eql?}).
|
|
#
|
|
# @return [Integer] the hash ID of `self`
|
|
def hash
|
|
[self.class, raw].hash
|
|
end
|
|
|
|
# Show a human-readable representation of `self`. To get a machine-
|
|
# readable "export" of the contained data, use {#as_json} or one of its
|
|
# aliases instead.
|
|
#
|
|
# @return [String] human-redable details about the object.
|
|
def inspect
|
|
id_str = Object.instance_method(:to_s).bind(self).call[2..-2]
|
|
"#<#{id_str} #{self}>"
|
|
end
|
|
|
|
# Provide a copy of the wrapped {#raw} data in a format allowing direct
|
|
# mapping to JSON.
|
|
#
|
|
# @note This method is usually overridden in child classes.
|
|
def as_json(*)
|
|
nil
|
|
end
|
|
|
|
# @return [String] a JSON document of the raw data. This method
|
|
# requires the JSON module to be required.
|
|
def to_json(*)
|
|
as_json.to_json
|
|
end
|
|
|
|
# @return [String] a string representation of {#as_json}.
|
|
def to_s
|
|
as_json.inspect
|
|
end
|
|
|
|
protected
|
|
|
|
attr_accessor :raw
|
|
|
|
private
|
|
|
|
def initialize_dup(source)
|
|
super
|
|
self.raw = source.raw.nil? ? nil : source.raw.dup
|
|
self
|
|
end
|
|
|
|
def initialize_clone(source)
|
|
super
|
|
self.raw = source.raw.nil? ? nil : source.raw.clone
|
|
self
|
|
end
|
|
|
|
def new(raw)
|
|
self.class.new.tap do |new_object|
|
|
new_object.raw = raw
|
|
end
|
|
end
|
|
|
|
def resolve_value(value, scope: nil)
|
|
return value unless Proc === value
|
|
|
|
return value.call if scope.nil?
|
|
value.arity == 0 ? scope.instance_exec(&value) : value.call(scope)
|
|
end
|
|
|
|
# Note: You should never mutate an array or hash returned by normalize
|
|
# when `wrap` is `false`.
|
|
def normalize(value, scope: nil, wrap: true)
|
|
value = resolve_value(value, scope: scope)
|
|
|
|
case value
|
|
when ::String
|
|
utf8(value)
|
|
when ::Symbol
|
|
utf8(value.to_s.freeze)
|
|
when ::Integer, ::Float
|
|
value
|
|
when true, false, nil
|
|
value
|
|
when ::Proc
|
|
resolved = resolve_value(value)
|
|
normalize(resolved, scope: scope, wrap: wrap)
|
|
when Rackstash::Fields::Hash, Rackstash::Fields::Array
|
|
wrap ? value : value.raw
|
|
when ::Hash
|
|
hash = {}
|
|
value.each_pair do |k, v|
|
|
hash[utf8(k)] = normalize(v, scope: scope)
|
|
end
|
|
if wrap
|
|
hash = Rackstash::Fields::Hash.new.tap do |hash_field|
|
|
hash_field.raw = hash
|
|
end
|
|
end
|
|
hash
|
|
when ::Array, ::Set, ::Enumerator
|
|
array = value.map { |e| normalize(e, scope: scope) }
|
|
if wrap
|
|
array = Rackstash::Fields::Array.new.tap do |array_field|
|
|
array_field.raw = array
|
|
end
|
|
end
|
|
array
|
|
when ::Time
|
|
value.getutc.iso8601(ISO8601_PRECISION).freeze
|
|
when ::DateTime
|
|
value.to_time.utc.iso8601(ISO8601_PRECISION).freeze
|
|
when ::Date
|
|
value.iso8601.encode!(Encoding::UTF_8).freeze
|
|
when ::Regexp, ::Range, ::URI::Generic, ::Pathname
|
|
utf8(value.to_s.freeze)
|
|
when Exception
|
|
exception = "#{value.message} (#{value.class})"
|
|
exception = [exception, *value.backtrace].join("\n") if value.backtrace
|
|
utf8(exception.freeze)
|
|
when ::BigDecimal
|
|
# A BigDecimal would be naturally represented as a JSON number. Most
|
|
# libraries, however, parse non-integer JSON numbers directly as
|
|
# floats. Clients using those libraries would get in general a wrong
|
|
# number and no way to recover other than manually inspecting the
|
|
# string with the JSON code itself.
|
|
value.to_s('F').encode!(Encoding::UTF_8).freeze
|
|
when ::Complex, ::Rational
|
|
# A complex number can not reliably converted to a float or rational,
|
|
# thus we always transform it to a String
|
|
utf8(value)
|
|
else
|
|
# Try to convert the value to a known basic type and recurse
|
|
converted = UNDEFINED
|
|
%i[
|
|
as_json
|
|
to_hash to_ary to_h to_a
|
|
to_time to_datetime to_date
|
|
to_f to_i
|
|
].each do |method|
|
|
# Try to convert the value to a base type but ignore any errors
|
|
begin
|
|
next unless value.respond_to?(method)
|
|
break converted = value.public_send(method)
|
|
rescue
|
|
next
|
|
end
|
|
end
|
|
|
|
if UNDEFINED.equal?(converted)
|
|
# The object doesn't seem to respond to any of the common converter
|
|
# methods. As a final effort, we try to inspect the object and use
|
|
# this value. If even inspecting fails (w.g. for BasicObjects), we
|
|
# try to force-inspect the object using our own inspect
|
|
# implementation.
|
|
converted = value.inspect rescue force_inspect(value)
|
|
end
|
|
|
|
normalize(converted, scope: scope, wrap: wrap)
|
|
end
|
|
end
|
|
|
|
def force_inspect(value)
|
|
obj_id_str_width = 0.size == 4 ? 7 : 14
|
|
obj_id = value.__id__ rescue 0
|
|
|
|
id_str = (obj_id << 1).to_s(16).rjust(obj_id_str_width, '0')
|
|
|
|
class_name = begin
|
|
value.class.name
|
|
rescue
|
|
Kernel.instance_method(:class).bind(value).call
|
|
end
|
|
|
|
"#<#{class_name}:0x#{id_str}>".freeze
|
|
end
|
|
end
|
|
end
|
|
end
|