You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

313 lines
8.2 KiB

#
# Copyright 2025 Splunk Inc.
#
# 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.
#
"""
Converters for Splunk configuration.
"""
import base64
import json
__all__ = [
"Converter",
"Normaliser",
"ChainOf",
"UserDefined",
"Unifier",
"Boolean",
"Lower",
"Upper",
"Mapping",
"Base64",
"JSON",
]
class Converter:
"""
Converting data: encode for in-coming request
and decode for out-coming response.
"""
def encode(self, value, request):
"""
Encode data from client for request.
:param value: value to encode for request
:param request: whole request data
:return:
"""
raise NotImplementedError()
def decode(self, value, response):
"""
Decode data from storage for response.
:param value: value to decode for response
:param response: whole response data
:return:
"""
raise NotImplementedError()
class Normaliser(Converter):
"""
Normalizing data: same converting logic for encode & decode.
"""
def normalize(self, value, data):
"""
Normalize a given value.
:param value: value to normalize
:param data: whole payload
:returns: normalized value.
"""
raise NotImplementedError()
def encode(self, value, request):
return self.normalize(value, request)
def decode(self, value, response):
return self.normalize(value, response)
class ChainOf(Converter):
"""
A composite of converters that will covert data with specified
converters on by one, and returns result from the last converter.
"""
def __init__(self, *converters):
"""
:param converters: a list of converters
"""
super().__init__()
self._converters = converters
def encode(self, value, request):
for converter in self._converters:
value = converter.encode(value, request)
return value
def decode(self, value, response):
import copy
converters = copy.copy(self._converters)
converters.reverse()
for converter in converters:
value = converter.decode(value, response)
return value
class UserDefined(Converter):
"""
User-defined normaliser.
The user-defined normaliser function should be in form:
``def fun(value, *args, **kwargs): ...``
Usage::
>>> def my_encoder(value, request, args):
>>> if request == args:
>>> return value
>>> else:
>>> return value
>>> my_converter = UserDefined(my_encoder, 'test_val')
>>> my_converter.encode('value', {'key': 'value'}, 'value1')
"""
def __init__(self, encoder, decoder=None, *args, **kwargs):
"""
:param encoder: user-defined function for encoding
:param decoder: user-defined function for decoding.
If None, it is the same to encoder.
:param args:
:param kwargs:
"""
super().__init__()
self._encoder = encoder
self._decoder = decoder or self._encoder
self._args = args
self._kwargs = kwargs
def encode(self, value, request):
return self._encoder(value, request, *self._args, **self._kwargs)
def decode(self, value, response):
return self._decoder(value, response, *self._args, **self._kwargs)
class Lower(Normaliser):
"""
Normalize a string to all lower cases.
"""
def normalize(self, value, data):
return value.strip().lower()
class Upper(Normaliser):
"""
Normalize a string to all upper cases.
"""
def normalize(self, value, data):
return value.strip().upper()
class Unifier(Normaliser):
"""
Many-to-one map for normalizing request & response.
"""
def __init__(
self,
value_map,
default=None,
case_sensitive=False,
):
"""
:param value_map:
{"<unified value>": "<original value list>"}
:param default: default value for input not in specific list
:param case_sensitive: if it is False,
it will return lower case
"""
super().__init__()
self._case_sensitive = case_sensitive
self._default = default
self._value_map = {}
for val_new, val_old_list in value_map.items():
for val_old in val_old_list:
val_old = val_old if case_sensitive else val_old.lower()
assert val_old not in self._value_map, (
'Normaliser "Unifier" only supports Many-to-one mapping: %s'
% val_old
)
self._value_map[val_old] = val_new
def normalize(self, value, data):
need_lower = not self._case_sensitive and isinstance(value, str)
val_old = value.lower() if need_lower else value
val_default = self._default or value
return self._value_map.get(val_old, val_default)
class Boolean(Unifier):
"""
Normalize a boolean field.
Normalize given value to boolean: 0 or 1
(for False and True respectively).
If the given value is not-a-string or unrecognizable,
it returns default value.
"""
VALUES_TRUE = {"true", "t", "1", "yes", "y"}
VALUES_FALSE = {"false", "f", "0", "no", "n"}
def __init__(self, default=True):
"""
:param default: default for unrecognizable input of boolean.
"""
super().__init__(
value_map={
"1": Boolean.VALUES_TRUE,
"0": Boolean.VALUES_FALSE,
},
default="1" if default else "0",
case_sensitive=False,
)
class Mapping(Converter):
"""
One-to-one map between interface value and storage value.
If value is not in specific mapping,
it will return the original value.
"""
def __init__(self, value_map, case_sensitive=False):
"""
:param value_map: {"<interface value>": "<storage value>"}
:param case_sensitive: if it is False,
it will return lower case
"""
super().__init__()
self._case_sensitive = case_sensitive
self._map_interface, self._map_storage = {}, {}
for interface, storage in value_map.items():
self._check_and_set(interface, storage)
def _check_and_set(self, interface, storage):
if not self._case_sensitive:
interface = interface.lower()
storage = storage.lower()
assert interface not in self._map_interface, (
'Converter "Mapping" only supports one-to-one mapping: "%s"' % interface
)
assert storage not in self._map_storage, (
'Converter "Mapping" only supports one-to-one mapping: "%s"' % storage
)
self._map_interface[interface] = storage
self._map_storage[storage] = interface
def encode(self, value, request):
if self._case_sensitive:
interface = value
else:
interface = value.lower()
return self._map_interface.get(interface, value)
def decode(self, value, response):
if self._case_sensitive:
storage = value
else:
storage = value.lower()
return self._map_storage.get(storage, value)
class Base64(Converter):
"""
Covert input data to base64 string.
"""
def encode(self, value, request):
return base64.b64encode(value)
def decode(self, value, response):
return base64.b64decode(value)
class JSON(Converter):
"""
Converter between object and JSON string.
"""
def encode(self, value, request):
return json.dumps(value)
def decode(self, value, response):
return json.loads(value)