lib/fluent/plugin_helper/record_accessor.rb
#
# 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