module Puppet::Pops
module Serialization
# Class that can process an arbitrary object into a value that is assignable to `Data`.
#
# @api public
class ToDataConverter
include Evaluator::Runtime3Support
# Convert the given _value_ according to the given _options_ and return the result of the conversion
#
# @param value [Object] the value to convert
# @param options {Symbol => <Boolean,String>} options hash
# @option options [Boolean] :rich_data `true` if rich data is enabled
# @option options [Boolean] :local_reference use local references instead of duplicating complex entries
# @option options [Boolean] :type_by_reference `true` if Object types are converted to references rather than embedded.
# @option options [Boolean] :symbol_as_string `true` if Symbols should be converted to strings (with type loss)
# @option options [Boolean] :force_symbol `false` if Symbols should not be converted (rich_data and symbol_as_string must be false)
# @option options [Boolean] :silence_warnings `false` if warnings should be silenced
# @option options [String] :message_prefix String to prepend to in warnings and errors
# @return [Data] the processed result. An object assignable to `Data`.
#
# @api public
def self.convert(value, options = EMPTY_HASH)
new(options).convert(value)
end
# Create a new instance of the processor
#
# @param options {Symbol => Object} options hash
# @option options [Boolean] :rich_data `true` if rich data is enabled
# @option options [Boolean] :local_references use local references instead of duplicating complex entries
# @option options [Boolean] :type_by_reference `true` if Object types are converted to references rather than embedded.
# @option options [Boolean] :symbol_as_string `true` if Symbols should be converted to strings (with type loss)
# @option options [String] :message_prefix String to prepend to path in warnings and errors
# @option semantic [Object] :semantic object to pass to the issue reporter
def initialize(options = EMPTY_HASH)
@type_by_reference = options[:type_by_reference]
@type_by_reference = true if @type_by_reference.nil?
@local_reference = options[:local_reference]
@local_reference = true if @local_reference.nil?
@symbol_as_string = options[:symbol_as_string]
@symbol_as_string = false if @symbol_as_string.nil?
@force_symbol = options[:force_symbol]
@force_symbol = false if @force_symbol.nil?
@silence_warnings = options[:silence_warnings]
@silence_warnings = false if @silence_warnings.nil?
@rich_data = options[:rich_data]
@rich_data = false if @rich_data.nil?
@message_prefix = options[:message_prefix]
@semantic = options[:semantic]
end
# Convert the given _value_
#
# @param value [Object] the value to convert
# @return [Data] the processed result. An object assignable to `Data`.
#
# @api public
def convert(value)
@path = []
@values = {}
to_data(value)
end
private
def path_to_s
s = @message_prefix || ''
s << JsonPath.to_json_path(@path)[1..-1]
s
end
def to_data(value)
if value.nil? || Types::PScalarDataType::DEFAULT.instance?(value)
if @rich_data && value.is_a?(String) && value.encoding == Encoding::ASCII_8BIT
# Transform the binary string to rich Binary
{
PCORE_TYPE_KEY => PCORE_TYPE_BINARY,
PCORE_VALUE_KEY => Puppet::Pops::Types::PBinaryType::Binary.from_binary_string(value).to_s
}
else
value
end
elsif :default == value
if @rich_data
{ PCORE_TYPE_KEY => PCORE_TYPE_DEFAULT }
else
serialization_issue(Issues::SERIALIZATION_DEFAULT_CONVERTED_TO_STRING, :path => path_to_s)
'default'
end
elsif value.is_a?(Symbol)
if @symbol_as_string
value.to_s
elsif @rich_data
{ PCORE_TYPE_KEY => PCORE_TYPE_SYMBOL, PCORE_VALUE_KEY => value.to_s }
else
if @force_symbol
value
else
@silence_warnings ? unknown_to_string(value) : unknown_to_string_with_warning(value)
end
end
elsif value.instance_of?(Array)
process(value) do
result = []
value.each_with_index do |elem, index|
with(index) { result << to_data(elem) }
end
result
end
elsif value.instance_of?(Hash)
process(value) do
if value.keys.all? { |key| key.is_a?(String) && key != PCORE_TYPE_KEY }
result = {}
value.each_pair { |key, elem| with(key) { result[key] = to_data(elem) } }
result
else
non_string_keyed_hash_to_data(value)
end
end
elsif value.instance_of?(Types::PSensitiveType::Sensitive)
process(value) do
{ PCORE_TYPE_KEY => PCORE_TYPE_SENSITIVE, PCORE_VALUE_KEY => to_data(value.unwrap) }
end
else
if @rich_data
value_to_data_hash(value)
else
@silence_warnings ? unknown_to_string(value) : unknown_to_string_with_warning(value)
end
end
end
# If `:local_references` is enabled, then the `object_id` will be associated with the current _path_ of
# the context the first time this method is called. The method then returns the result of yielding to
# the given block. Subsequent calls with a value that has the same `object_id` will instead return a
# reference based on the given path.
#
# If `:local_references` is disabled, then this method performs a check for endless recursion before
# it yields to the given block. The result of yielding is returned.
#
# @param value [Object] the value
# @yield The block that will produce the data for the value
# @return [Data] the result of yielding to the given block, or a hash denoting a reference
#
# @api private
def process(value, &block)
if @local_reference
id = value.object_id
ref = @values[id]
if ref.nil?
@values[id] = @path.dup
yield
elsif ref.instance_of?(Hash)
ref
else
json_ref = JsonPath.to_json_path(ref)
if json_ref.nil?
# Complex key and hence no way to reference the prior value. The value must therefore be
# duplicated which in turn introduces a risk for endless recursion in case of self
# referencing structures
with_recursive_guard(value, &block)
else
@values[id] = { PCORE_TYPE_KEY => PCORE_LOCAL_REF_SYMBOL, PCORE_VALUE_KEY => json_ref }
end
end
else
with_recursive_guard(value, &block)
end
end
# Pushes `key` to the end of the path and yields to the given block. The
# `key` is popped when the yield returns.
# @param key [Object] the key to push on the current path
# @yield The block that will produce the returned value
# @return [Object] the result of yielding to the given block
#
# @api private
def with(key)
@path.push(key)
value = yield
@path.pop
value
end
# @param value [Object] the value to use when checking endless recursion
# @yield The block that will produce the data
# @return [Data] the result of yielding to the given block
def with_recursive_guard(value)
id = value.object_id
if @recursive_lock
if @recursive_lock.include?(id)
serialization_issue(Issues::SERIALIZATION_ENDLESS_RECURSION, :type_name => value.class.name)
end
@recursive_lock[id] = true
else
@recursive_lock = { id => true }
end
v = yield
@recursive_lock.delete(id)
v
end
def unknown_key_to_string_with_warning(value)
str = unknown_to_string(value)
serialization_issue(Issues::SERIALIZATION_UNKNOWN_KEY_CONVERTED_TO_STRING, :path => path_to_s, :klass => value.class, :value => str)
str
end
def unknown_to_string_with_warning(value)
str = unknown_to_string(value)
serialization_issue(Issues::SERIALIZATION_UNKNOWN_CONVERTED_TO_STRING, :path => path_to_s, :klass => value.class, :value => str)
str
end
def unknown_to_string(value)
value.is_a?(Regexp) ? Puppet::Pops::Types::PRegexpType.regexp_to_s_with_delimiters(value) : value.to_s
end
def non_string_keyed_hash_to_data(hash)
if @rich_data
to_key_extended_hash(hash)
else
result = {}
hash.each_pair do |key, value|
if key.is_a?(Symbol) && @symbol_as_string
key = key.to_s
elsif !key.is_a?(String)
key = unknown_key_to_string_with_warning(key)
end
with(key) { result[key] = to_data(value) }
end
result
end
end
# A Key extended hash is a hash whose keys are not entirely strings. Such a hash
# cannot be safely represented as JSON or YAML
#
# @param hash {Object => Object} the hash to process
# @return [String => Data] the representation of the extended hash
def to_key_extended_hash(hash)
key_value_pairs = []
hash.each_pair do |key, value|
key = to_data(key)
key_value_pairs << key
key_value_pairs << with(key) { to_data(value) }
end
{ PCORE_TYPE_KEY => PCORE_TYPE_HASH, PCORE_VALUE_KEY => key_value_pairs }
end
def value_to_data_hash(value)
pcore_type = value.is_a?(Types::PuppetObject) ? value._pcore_type : Types::TypeCalculator.singleton.infer(value)
if pcore_type.is_a?(Puppet::Pops::Types::PRuntimeType)
unknown_to_string_with_warning(value)
else
pcore_tv = pcore_type_to_data(pcore_type)
if pcore_type.roundtrip_with_string?
{
PCORE_TYPE_KEY => pcore_tv,
# Scalar values are stored using their default string representation
PCORE_VALUE_KEY => Types::StringConverter.singleton.convert(value)
}
elsif pcore_type.implementation_class.respond_to?(:_pcore_init_from_hash)
process(value) do
{
PCORE_TYPE_KEY => pcore_tv,
}.merge(to_data(value._pcore_init_hash))
end
else
process(value) do
(names, _, required_count) = pcore_type.parameter_info(value.class)
args = names.map { |name| value.send(name) }
# Pop optional arguments that are default
while args.size > required_count
break unless pcore_type[names[args.size-1]].default_value?(args.last)
args.pop
end
result = {
PCORE_TYPE_KEY => pcore_tv
}
args.each_with_index do |val, idx|
key = names[idx]
with(key) { result[key] = to_data(val) }
end
result
end
end
end
end
def pcore_type_to_data(pcore_type)
type_name = pcore_type.name
if @type_by_reference || type_name.start_with?('Pcore::')
type_name
else
with(PCORE_TYPE_KEY) { to_data(pcore_type) }
end
end
private :pcore_type_to_data
def serialization_issue(issue, options = EMPTY_HASH)
semantic = @semantic
if semantic.nil?
tos = Puppet::Pops::PuppetStack.top_of_stack
if tos.empty?
semantic = Puppet::Pops::SemanticError.new(issue, nil, EMPTY_HASH)
else
file, line = stacktrace
semantic = Puppet::Pops::SemanticError.new(issue, nil, {:file => file, :line => line})
end
end
optionally_fail(issue, semantic, options)
end
end
end
end
Copyright 2K16 - 2K18 Indonesian Hacker Rulez