Repository: janehmueller/python-json-config

Issue:
Chain validator functions
It would be great if one would be able to execute several validator functions in succession. So instead of having something like:
```
builder.validate_field_value('server.port', function_1)
```
it would be great if one could do:
```
builder.validate_field_value('server.port', [function_1, function_2])
```
... so that the specified validators get executed one after another.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 2868625..28e89e0 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,11 @@ builder.validate_field_value('server.ip', is_ipv4_address)
 builder.validate_field_value('server.port', is_unreserved_port)
 builder.validate_field_value('jwt.access_token_expires', is_timedelta)
 
-# you can also return custom error messages in your lambdas
-builder.validate_field_value('server.ip', lambda ip: (ip != "0.0.0.0", "IP is unroutable."))
+# return custom error messages in your lambdas
+builder.validate_field_value('server.ip', lambda ip: (ip != '0.0.0.0', 'IP is unroutable.'))
+
+# chain validation functions
+builder.validate_field_value('server.ip', [lambda ip: ip != 'localhost', lambda ip: ip != '127.0.0.1'])
 
 # parse a timedelta (e.g., Jun 1 2005) into a datetime object
 builder.transform_field_value('important_date', lambda date: datetime.strptime(date, '%b %d %Y'))
@@ -45,7 +48,7 @@ port = config.server.port
 assert port > 1023
 
 ip = config.server.ip
-assert ip != "0.0.0.0"
+assert ip not in ['0.0.0.0', 'localhost', '127.0.0.1']
 
 important_date = config.important_date
 assert isinstance(important_date, datetime)
diff --git a/python_json_config/config_builder.py b/python_json_config/config_builder.py
index 2e219b3..d7aa0f3 100644
--- a/python_json_config/config_builder.py
+++ b/python_json_config/config_builder.py
@@ -1,12 +1,13 @@
 import json
+from typing import Dict
 
-from .config_node import ConfigNode, Config
+from .config_node import Config
 
 
 class ConfigBuilder(object):
     def __init__(self):
-        self.__validation_types = {}
-        self.__validation_functions = {}
+        self.__validation_types: Dict[str, type] = {}
+        self.__validation_functions: Dict[str, list] = {}
         self.__transformation_functions = {}
         self.__config: Config = None
 
@@ -15,7 +16,14 @@ class ConfigBuilder(object):
         return self
 
     def validate_field_value(self, field_name: str, validation_function):
-        self.__validation_functions[field_name] = validation_function
+        if field_name not in self.__validation_functions:
+            self.__validation_functions[field_name] = []
+
+        if isinstance(validation_function, list):
+            self.__validation_functions[field_name] += validation_function
+        else:
+            self.__validation_functions[field_name].append(validation_function)
+
         return self
 
     def transform_field_value(self, field_name: str, transformation_function):
@@ -38,16 +46,17 @@ class ConfigBuilder(object):
             assert isinstance(value, field_type), f'Config field "{field_name}" with value "{value}" is not of type {field_type}'
 
     def __validate_field_values(self):
-        for field_name, validation_function in self.__validation_functions.items():
+        for field_name, validation_functions in self.__validation_functions.items():
             value = self.__config.get(field_name)
-            validation_result = validation_function(value)
-            error_message = f'Error validating field "{field_name}" with value "{value}"'
-
-            if isinstance(validation_result, tuple):
-                result, validation_error = validation_result
-                assert result, f"{error_message}: {validation_error}"
-            else:
-                assert validation_result, error_message
+            for validation_function in validation_functions:
+                validation_result = validation_function(value)
+                error_message = f'Error validating field "{field_name}" with value "{value}"'
+
+                if isinstance(validation_result, tuple):
+                    result, validation_error = validation_result
+                    assert result, f"{error_message}: {validation_error}"
+                else:
+                    assert validation_result, error_message
 
     def __transform_field_values(self):
         for field_name, transformation_function in self.__transformation_functions.items():


Test patch:
diff --git a/tests/config_builder_test.py b/tests/config_builder_test.py
index ec267b2..5d8a22e 100644
--- a/tests/config_builder_test.py
+++ b/tests/config_builder_test.py
@@ -1,7 +1,6 @@
 from unittest import TestCase
 
 from python_json_config import ConfigBuilder
-from python_json_config.validators import is_unreserved_port
 
 
 class ConfigBuilderTest(TestCase):
@@ -32,10 +31,10 @@ class ConfigBuilderTest(TestCase):
         builder.validate_field_value('key1', lambda x: x < 4)
         builder.validate_field_value('key1.key2', lambda x: len(x) == 3)
         value_validation_dict = builder._ConfigBuilder__validation_functions
-        self.assertTrue(value_validation_dict['key1'](1))
-        self.assertFalse(value_validation_dict['key1'](4))
-        self.assertTrue(value_validation_dict['key1.key2']([1, 2, 3]))
-        self.assertFalse(value_validation_dict['key1.key2'](['a', 'b']))
+        self.assertTrue(value_validation_dict['key1'][0](1))
+        self.assertFalse(value_validation_dict['key1'][0](4))
+        self.assertTrue(value_validation_dict['key1.key2'][0]([1, 2, 3]))
+        self.assertFalse(value_validation_dict['key1.key2'][0](['a', 'b']))
 
     def test_value_transformation_builder(self):
         builder = ConfigBuilder()
@@ -59,9 +58,15 @@ class ConfigBuilderTest(TestCase):
     def test_value_validation(self):
         builder = ConfigBuilder()
         builder.validate_field_value('server.debug_mode', lambda x: not x)
-        builder.validate_field_value('server.port', lambda x: x < 10000)
+        builder.validate_field_value('server.port', [lambda x: x < 10000, lambda x: x > 1023])
         builder.parse_config(self.path)
-        builder.validate_field_value('cache.ttl', lambda x: x > 200)
+
+        builder.validate_field_value('server.port', lambda x: x > 6000)
+        with self.assertRaises(AssertionError):
+            builder.parse_config(self.path)
+
+        builder = ConfigBuilder()
+        builder = builder.validate_field_value('cache.ttl', lambda x: x > 200)
         with self.assertRaises(AssertionError):
             builder.parse_config(self.path)