feat(shared): load env config using jsonschema

The env loader is now capable of loading lists of objects, union types
or list of union types from the env variables.
They are some limitations: for example it doesn't support unions of
different shapes `list | dict` or `str | dict`.
This commit is contained in:
jo 2022-07-30 21:15:39 +02:00 committed by Kyle Robbertze
parent 6c449e3019
commit bcd877266f
3 changed files with 500 additions and 47 deletions

View file

@ -0,0 +1,189 @@
# pylint: disable=protected-access
from os import environ
from typing import List, Union
from unittest import mock
import pytest
from pydantic import BaseModel
from libretime_shared.config import BaseConfig
from libretime_shared.config._env import EnvLoader
ENV_SCHEMA_OBJ_WITH_STR = {
"type": "object",
"properties": {"a_str": {"type": "string"}},
}
@pytest.mark.parametrize(
"env_parent, env, schema, expected",
[
(
"PRE",
{"PRE_A_STR": "found"},
{"a_str": {"type": "string"}},
{"a_str": "found"},
),
(
"PRE",
{"PRE_OBJ_A_STR": "found"},
{"obj": ENV_SCHEMA_OBJ_WITH_STR},
{"obj": {"a_str": "found"}},
),
(
"PRE",
{"PRE_ARR1": "one, two"},
{"arr1": {"type": "array", "items": {"type": "string"}}},
{"arr1": ["one", "two"]},
),
(
"PRE",
{
"PRE_ARR2_0_A_STR": "one",
"PRE_ARR2_1_A_STR": "two",
"PRE_ARR2_3_A_STR": "ten",
},
{"arr2": {"type": "array", "items": ENV_SCHEMA_OBJ_WITH_STR}},
{
"arr2": [
{"a_str": "one"},
{"a_str": "two"},
None,
{"a_str": "ten"},
]
},
),
],
)
def test_env_config_loader_get_object(
env_parent,
env,
schema,
expected,
):
with mock.patch.dict(environ, env):
loader = EnvLoader(schema={}, env_prefix="PRE")
result = loader._get_object(env_parent, {"properties": schema})
assert result == expected
class FirstChildConfig(BaseModel):
a_child_str: str
class SecondChildConfig(BaseModel):
a_child_str: str
a_child_int: int
# pylint: disable=too-few-public-methods
class FixtureConfig(BaseConfig):
a_str: str
a_list_of_str: List[str]
a_obj: FirstChildConfig
a_obj_with_default: FirstChildConfig = FirstChildConfig(a_child_str="default")
a_list_of_obj: List[FirstChildConfig]
a_union_str_or_int: Union[str, int]
a_union_obj: Union[FirstChildConfig, SecondChildConfig]
a_list_of_union_str_or_int: List[Union[str, int]]
a_list_of_union_obj: List[Union[FirstChildConfig, SecondChildConfig]]
ENV_SCHEMA = FixtureConfig.schema()
@pytest.mark.parametrize(
"env_name, env, schema, expected",
[
(
"PRE_A_STR",
{"PRE_A_STR": "found"},
ENV_SCHEMA["properties"]["a_str"],
"found",
),
(
"PRE_A_LIST_OF_STR",
{"PRE_A_LIST_OF_STR": "one, two"},
ENV_SCHEMA["properties"]["a_list_of_str"],
["one", "two"],
),
(
"PRE_A_OBJ",
{"PRE_A_OBJ_A_CHILD_STR": "found"},
ENV_SCHEMA["properties"]["a_obj"],
{"a_child_str": "found"},
),
],
)
def test_env_config_loader_get(
env_name,
env,
schema,
expected,
):
with mock.patch.dict(environ, env):
loader = EnvLoader(schema=ENV_SCHEMA, env_prefix="PRE")
result = loader._get(env_name, schema)
assert result == expected
def test_env_config_loader_load_empty():
with mock.patch.dict(environ, {}):
loader = EnvLoader(schema=ENV_SCHEMA, env_prefix="PRE")
result = loader.load()
assert not result
def test_env_config_loader_load():
with mock.patch.dict(
environ,
{
"PRE_A_STR": "found",
"PRE_A_LIST_OF_STR": "one, two",
"PRE_A_OBJ": "invalid",
"PRE_A_OBJ_A_CHILD_STR": "found",
"PRE_A_OBJ_WITH_DEFAULT_A_CHILD_STR": "found",
"PRE_A_LIST_OF_OBJ": "invalid",
"PRE_A_LIST_OF_OBJ_0_A_CHILD_STR": "found",
"PRE_A_LIST_OF_OBJ_1_A_CHILD_STR": "found",
"PRE_A_LIST_OF_OBJ_3_A_CHILD_STR": "found",
"PRE_A_LIST_OF_OBJ_INVALID": "invalid",
"PRE_A_UNION_STR_OR_INT": "found",
"PRE_A_UNION_OBJ_A_CHILD_STR": "found",
"PRE_A_UNION_OBJ_A_CHILD_INT": "found",
"PRE_A_LIST_OF_UNION_STR_OR_INT": "one, two, 3",
"PRE_A_LIST_OF_UNION_STR_OR_INT_3": "4",
"PRE_A_LIST_OF_UNION_OBJ": "invalid",
"PRE_A_LIST_OF_UNION_OBJ_0_A_CHILD_STR": "found",
"PRE_A_LIST_OF_UNION_OBJ_1_A_CHILD_STR": "found",
"PRE_A_LIST_OF_UNION_OBJ_1_A_CHILD_INT": "found",
"PRE_A_LIST_OF_UNION_OBJ_3_A_CHILD_INT": "found",
"PRE_A_LIST_OF_UNION_OBJ_INVALID": "invalid",
},
):
loader = EnvLoader(schema=ENV_SCHEMA, env_prefix="PRE")
result = loader.load()
assert result == {
"a_str": "found",
"a_list_of_str": ["one", "two"],
"a_obj": {"a_child_str": "found"},
"a_obj_with_default": {"a_child_str": "found"},
"a_list_of_obj": [
{"a_child_str": "found"},
{"a_child_str": "found"},
None,
{"a_child_str": "found"},
],
"a_union_str_or_int": "found",
"a_union_obj": {
"a_child_str": "found",
"a_child_int": "found",
},
"a_list_of_union_str_or_int": ["one", "two", "3", "4"],
"a_list_of_union_obj": [
{"a_child_str": "found"},
{"a_child_str": "found", "a_child_int": "found"},
None,
{"a_child_int": "found"},
],
}