fluent/fluentd

View on GitHub
lib/fluent/plugin_helper/record_accessor.rb

Summary

Maintainability
C
1 day
Test Coverage
#
# Fluentd
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#

require 'fluent/config/error'

module Fluent
  module PluginHelper
    module RecordAccessor
      def record_accessor_create(param)
        Accessor.new(param)
      end

      def record_accessor_nested?(param)
        Accessor.parse_parameter(param).is_a?(Array)
      end

      class Accessor
        attr_reader :keys

        def initialize(param)
          @keys = Accessor.parse_parameter(param)

          if @keys.is_a?(Array)
            @last_key = @keys.last
            @dig_keys = @keys[0..-2]
            if @dig_keys.empty?
              @keys = @keys.first
            else
              mcall = method(:call_dig)
              mdelete = method(:delete_nest)
              mset = method(:set_nest)
              singleton_class.module_eval do
                define_method(:call, mcall)
                define_method(:delete, mdelete)
                define_method(:set, mset)
              end
            end
          end
        end

        def call(r)
          r[@keys]
        end

        # To optimize the performance, use class_eval with pre-expanding @keys
        # See https://gist.github.com/repeatedly/ab553ed260cd080bd01ec71da9427312
        def call_dig(r)
          r.dig(*@keys)
        end

        def delete(r)
          r.delete(@keys)
        end

        def delete_nest(r)
          if target = r.dig(*@dig_keys)
            if target.is_a?(Array)
              target.delete_at(@last_key)
            else
              target.delete(@last_key)
            end
          end
        rescue
          nil
        end

        def set(r, v)
          r[@keys] = v
        end

        # set_nest doesn't create intermediate object. If key doesn't exist, no effect.
        # See also: https://bugs.ruby-lang.org/issues/11747
        def set_nest(r, v)
          r.dig(*@dig_keys)&.[]=(@last_key, v)
        rescue
          nil
        end

        def self.parse_parameter(param)
          if param.start_with?('$.')
            parse_dot_notation(param)
          elsif param.start_with?('$[')
            parse_bracket_notation(param)
          else
            param
          end
        end

        def self.parse_dot_notation(param)
          result = []
          keys = param[2..-1].split('.')
          keys.each { |key|
            if key.include?('[')
              result.concat(parse_dot_array_op(key, param))
            else
              result << key
            end
          }

          raise Fluent::ConfigError, "empty keys in dot notation" if result.empty?
          validate_dot_keys(result)

          result
        end

        def self.validate_dot_keys(keys)
          keys.each { |key|
            next unless key.is_a?(String)
            if /\s+/.match?(key)
              raise Fluent::ConfigError, "whitespace character is not allowed in dot notation. Use bracket notation: #{key}"
            end
          }
        end

        def self.parse_dot_array_op(key, param)
          start = key.index('[')
          result = if start.zero?
                     []
                   else
                     [key[0..start - 1]]
                   end
          key = key[start + 1..-1]
          in_bracket = true

          until key.empty?
            if in_bracket
              if i = key.index(']')
                index_value = key[0..i - 1]
                raise Fluent::ConfigError, "missing array index in '[]'. Invalid syntax: #{param}" if index_value == ']'
                result << Integer(index_value)
                key = key[i + 1..-1]
                in_bracket = false
              else
                raise Fluent::ConfigError, "'[' found but ']' not found. Invalid syntax: #{param}"
              end
            else
              if i = key.index('[')
                key = key[i + 1..-1]
                in_bracket = true
              else
                raise Fluent::ConfigError, "found more characters after ']'. Invalid syntax: #{param}"
              end
            end
          end

          result
        end

        def self.parse_bracket_notation(param)
          orig_param = param
          result = []
          param = param[1..-1]
          in_bracket = false

          until param.empty?
            if in_bracket
              if param[0] == "'" || param[0] == '"'
                if i = param.index("']") || param.index('"]')
                  raise Fluent::ConfigError, "Mismatched quotes. Invalid syntax: #{orig_param}" unless param[0] == param[i]
                  result << param[1..i - 1]
                  param = param[i + 2..-1]
                  in_bracket = false
                else
                  raise Fluent::ConfigError, "Incomplete bracket. Invalid syntax: #{orig_param}"
                end
              else
                if i = param.index(']')
                  index_value = param[0..i - 1]
                  raise Fluent::ConfigError, "missing array index in '[]'. Invalid syntax: #{param}" if index_value == ']'
                  result << Integer(index_value)
                  param = param[i + 1..-1]
                  in_bracket = false
                else
                  raise Fluent::ConfigError, "'[' found but ']' not found. Invalid syntax: #{orig_param}"
                end
              end
            else
              if i = param.index('[')
                param = param[i + 1..-1]
                in_bracket = true
              else
                raise Fluent::ConfigError, "found more characters after ']'. Invalid syntax: #{orig_param}"
              end
            end
          end

          raise Fluent::ConfigError, "empty keys in bracket notation" if result.empty?

          result
        end
      end
    end
  end
end