# frozen_string_literal: true

# Digs into a value with dot notation to get a value from within a structure.
#
# **To dig into a given value**, call the function with (at least) two arguments:
#
# * The **first** argument must be an Array, or Hash. Value can also be `undef`
#   (which also makes the result `undef` unless a _default value_ is given).
# * The **second** argument must be a _dot notation navigation string_.
# * The **optional third** argument can be any type of value and it is used
#   as the _default value_ if the function would otherwise return `undef`.
# * An **optional lambda** for error handling taking one `Error` argument.
#
# **Dot notation navigation string** -
# The dot string consists of period `.` separated segments where each
# segment is either the index into an array or the value of a hash key.
# If a wanted key contains a period it must be quoted to avoid it being
# taken as a segment separator. Quoting can be done with either
# single quotes `'` or double quotes `"`. If a segment is
# a decimal number it is converted to an Integer index. This conversion
# can be prevented by quoting the value.
#
# @example Navigating into a value
# ```puppet
# #get($facts, 'os.family')
# $facts.get('os.family')
# ```
# Would both result in the value of `$facts['os']['family']`
#
# @example Getting the value from an expression
# ```puppet
# get([1,2,[{'name' =>'waldo'}]], '2.0.name')
# ```
# Would result in `'waldo'`
#
# @example Using a default value
# ```puppet
# get([1,2,[{'name' =>'waldo'}]], '2.1.name', 'not waldo')
#
# ```
# Would result in `'not waldo'`
#
# @example Quoting a key with period
# ```puppet
# $x = [1, 2, { 'readme.md' => "This is a readme."}]
# $x.get('2."readme.md"')
# ```
#
# @example Quoting a numeric string
# ```puppet
# $x = [1, 2, { '10' => "ten"}]
# $x.get('2."0"')
# ```
#
# **Error Handling** - There are two types of common errors that can
# be handled by giving the function a code block to execute.
# (A third kind or error; when the navigation string has syntax errors
# (for example an empty segment or unbalanced quotes) will always raise
# an error).
#
# The given block will be given an instance of the `Error` data type,
# and it has methods to extract `msg`, `issue_code`, `kind`, and
# `details`.
#
# The `msg` will be a preformatted message describing the error.
# This is the error message that would have surfaced if there was
# no block to handle the error.
#
# The `kind` is the string `'SLICE_ERROR'` for both kinds of errors,
# and the `issue_code` is either the string `'EXPECTED_INTEGER_INDEX'`
# for an attempt to index into an array with a String,
# or `'EXPECTED_COLLECTION'` for an attempt to index into something that
# is not a Collection.
#
# The `details` is a Hash that for both issue codes contain the
# entry `'walked_path'` which is an Array with each key in the
# progression of the dig up to the place where the error occurred.
#
# For an `EXPECTED_INTEGER_INDEX`-issue the detail `'index_type'` is
# set to the data type of the index value and for an
# `'EXPECTED_COLLECTION'`-issue the detail `'value_type'` is set
# to the type of the value.
#
# The logic in the error handling block can inspect the details,
# and either call `fail()` with a custom error message or produce
# the wanted value.
#
# If the block produces `undef` it will not be replaced with a
# given default value.
#
# @example Ensure `undef` result on error
# ```puppet
# $x = 'blue'
# $x.get('0.color', 'green') |$error| { undef } # result is undef
#
# $y = ['blue']
# $y.get('color', 'green') |$error| { undef } # result is undef
# ```
#
# @example Accessing information in the Error
# ```puppet
# $x = [1, 2, ['blue']]
# $x.get('2.color') |$error| {
#   notice("Walked path is ${error.details['walked_path']}")
# }
# ```
# Would notice `Walked path is [2, color]`
#
# Also see:
# * `getvar()` that takes the first segment to be the name of a variable
#   and then delegates to this function.
# * `dig()` function which is similar but uses an
#   array of navigation values instead of a dot notation string.
#
# @since 6.0.0
#
Puppet::Functions.create_function(:get, Puppet::Functions::InternalFunction) do
  dispatch :get_from_value do
    param 'Any', :value
    param 'String', :dotted_string
    optional_param 'Any', :default_value
    optional_block_param 'Callable[1,1]', :block
  end

  # Gets a result from given value and a navigation string
  #
  def get_from_value(value, navigation, default_value = nil, &block)
    return default_value if value.nil?
    return value if navigation.empty?

    # Note: split_key always processes the initial segment as a string even if it could be an integer.
    # This since it is designed for lookup keys. For a numeric first segment
    # like '0.1' the wanted result is `[0,1]`, not `["0", 1]`. The workaround here is to
    # prefix the navigation with `"x."` thus giving split_key a first segment that is a string.
    # The fake segment is then dropped.
    segments = split_key("x." + navigation) { |_err| _("Syntax error in dotted-navigation string") }
    segments.shift

    begin
      result = call_function('dig', value, *segments)
      result.nil? ? default_value : result
    rescue Puppet::ErrorWithData => e
      if block_given?
        yield(e.error_data)
      else
        raise e
      end
    end
  end
  # reuse the split_key parser used also by lookup
  include Puppet::Pops::Lookup::SubLookup
end
