Skip to content

Commit 318519f

Browse files
authored
Add ConfigResolver with necessary tests (#641)
* Add a ConfigResolver that goes through the list of sources to resolve config value
1 parent 3ddcc64 commit 318519f

4 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from .resolver import ConfigResolver
5+
6+
__all__ = [
7+
"ConfigResolver",
8+
]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from collections.abc import Sequence
4+
from typing import Any
5+
6+
from smithy_core.interfaces.config import ConfigSource
7+
8+
9+
class ConfigResolver:
10+
"""Resolves configuration values from multiple sources.
11+
12+
The resolver iterates through sources in precedence order, returning
13+
the first non-None value found for a given configuration key.
14+
"""
15+
16+
def __init__(self, sources: Sequence[ConfigSource]) -> None:
17+
"""Initialize the resolver with sources in precedence order.
18+
19+
:param sources: List of configuration sources in precedence order. The first
20+
source in the list has the highest priority.
21+
"""
22+
self._sources = sources
23+
24+
def get(self, key: str) -> tuple[Any, str | None]:
25+
"""Resolve a configuration value from sources by iterating through them in precedence order.
26+
27+
:param key: The configuration key to resolve (e.g., 'retry_mode')
28+
29+
:returns: A tuple of (value, source_name). If no source provides a value,
30+
returns (None, None).
31+
"""
32+
for source in self._sources:
33+
value = source.get(key)
34+
if value is not None:
35+
return (value, source.name)
36+
return (None, None)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Any
4+
5+
from smithy_core.config.resolver import ConfigResolver
6+
7+
8+
class StubSource:
9+
"""A simple ConfigSource implementation for testing.
10+
11+
Returns values from a provided dictionary, or None if the key
12+
is not present.
13+
"""
14+
15+
def __init__(self, source_name: str, data: dict[str, Any] | None = None):
16+
self._name = source_name
17+
self._data = data or {}
18+
19+
@property
20+
def name(self) -> str:
21+
return self._name
22+
23+
def get(self, key: str) -> Any | None:
24+
return self._data.get(key)
25+
26+
27+
class TestConfigResolver:
28+
def test_returns_value_from_single_source(self):
29+
source = StubSource("environment", {"region": "us-west-2"})
30+
resolver = ConfigResolver(sources=[source])
31+
32+
result = resolver.get("region")
33+
34+
assert result == ("us-west-2", "environment")
35+
36+
def test_returns_None_when_source_has_no_value(self):
37+
source = StubSource("environment", {})
38+
resolver = ConfigResolver(sources=[source])
39+
40+
result = resolver.get("region")
41+
42+
assert result == (None, None)
43+
44+
def test_returns_None_with_empty_source_list(self):
45+
resolver = ConfigResolver(sources=[])
46+
47+
result = resolver.get("region")
48+
49+
assert result == (None, None)
50+
51+
def test_first_source_takes_precedence(self):
52+
first_priority_source = StubSource("source_one", {"region": "us-east-1"})
53+
second_priority_source = StubSource("source_two", {"region": "eu-west-1"})
54+
resolver = ConfigResolver(
55+
sources=[first_priority_source, second_priority_source]
56+
)
57+
58+
result = resolver.get("region")
59+
60+
assert result == ("us-east-1", "source_one")
61+
62+
def test_skips_source_returning_none_and_uses_next(self):
63+
empty_source = StubSource("source_one", {})
64+
fallback_source = StubSource("source_two", {"region": "ap-south-1"})
65+
resolver = ConfigResolver(sources=[empty_source, fallback_source])
66+
67+
result = resolver.get("region")
68+
69+
assert result == ("ap-south-1", "source_two")
70+
71+
def test_resolves_different_keys_from_different_sources(self):
72+
instance = StubSource("source_one", {"region": "us-west-2"})
73+
environment = StubSource("source_two", {"retry_mode": "adaptive"})
74+
resolver = ConfigResolver(sources=[instance, environment])
75+
76+
region = resolver.get("region")
77+
retry_mode = resolver.get("retry_mode")
78+
79+
assert region == ("us-west-2", "source_one")
80+
assert retry_mode == ("adaptive", "source_two")
81+
82+
def test_returns_non_string_values(self):
83+
source = StubSource(
84+
"default",
85+
{
86+
"max_retries": 3,
87+
"use_ssl": True,
88+
},
89+
)
90+
resolver = ConfigResolver(sources=[source])
91+
92+
assert resolver.get("max_retries") == (3, "default")
93+
assert resolver.get("use_ssl") == (True, "default")
94+
95+
def test_get_is_idempotent(self):
96+
source = StubSource("environment", {"region": "us-west-2"})
97+
resolver = ConfigResolver(sources=[source])
98+
99+
result1 = resolver.get("region")
100+
result2 = resolver.get("region")
101+
result3 = resolver.get("region")
102+
103+
assert result1 == result2 == result3 == ("us-west-2", "environment")
104+
105+
def test_treats_empty_string_as_valid_value(self):
106+
source = StubSource("test", {"region": ""})
107+
resolver = ConfigResolver(sources=[source])
108+
109+
value, source_name = resolver.get("region")
110+
111+
assert value == ""
112+
assert source_name == "test"

0 commit comments

Comments
 (0)