diff --git a/switcher_client/client.py b/switcher_client/client.py index 0e9e55e..33f0a36 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -4,7 +4,7 @@ from switcher_client.lib.remote_auth import RemoteAuth from switcher_client.lib.globals.global_context import Context, ContextOptions from switcher_client.lib.globals.global_context import DEFAULT_ENVIRONMENT -from switcher_client.lib.snapshot import load_domain +from switcher_client.lib.snapshot_loader import load_domain from switcher_client.lib.utils import get from switcher_client.switcher import Switcher diff --git a/switcher_client/lib/snapshot.py b/switcher_client/lib/snapshot.py deleted file mode 100644 index 8b76c89..0000000 --- a/switcher_client/lib/snapshot.py +++ /dev/null @@ -1,16 +0,0 @@ -from switcher_client.lib.types import Snapshot, SnapshotData, Domain - - -def load_domain(snpahsot_location: str, environment: str): - """ Load Domain from snapshot file """ - - snapshot_file = f"{snpahsot_location}/{environment}.json" - - print(f"Loading snapshot from {snapshot_file}") - snapshot_data = SnapshotData() - snapshot_data.domain = Domain() - snapshot_data.domain.version = 1 - - snapshot = Snapshot() - snapshot.data = snapshot_data - return snapshot \ No newline at end of file diff --git a/switcher_client/lib/snapshot_loader.py b/switcher_client/lib/snapshot_loader.py new file mode 100644 index 0000000..557f3a4 --- /dev/null +++ b/switcher_client/lib/snapshot_loader.py @@ -0,0 +1,100 @@ +import json +import os + +from switcher_client.lib.types import Snapshot, SnapshotData, Domain, Group, Config, StrategyConfig, Relay + +def load_domain(snapshot_location: str, environment: str): + """ Load Domain from snapshot file """ + + snapshot_file = f"{snapshot_location}/{environment}.json" + json_data = {} + + if not os.path.exists(snapshot_file): + json_data = { + 'data': { + 'domain': { + 'version': 0, + } + } + } + + if snapshot_location: + os.makedirs(snapshot_location, exist_ok=True) + with open(snapshot_file, 'w') as file: + json.dump(json_data, file, indent=4) + + elif os.path.exists(snapshot_file): + with open(snapshot_file, 'r') as file: + json_data = json.load(file) + + snapshot = Snapshot() + snapshot.data = SnapshotData() + snapshot.data.domain = _parse_domain(json_data['data']['domain']) + + return snapshot + +def _parse_domain(domain_data: dict) -> Domain: + """ Parse domain data from JSON """ + + domain = Domain() + domain.name = domain_data.get('name') + domain.activated = domain_data.get('activated') + domain.version = domain_data.get('version', 0) + + if 'group' in domain_data and domain_data['group']: + domain.group = [] + for group_data in domain_data['group']: + domain.group.append(_parse_group(group_data)) + + return domain + +def _parse_group(group_data: dict) -> Group: + """ Parse group data from JSON """ + + group = Group() + group.name = group_data.get('name') + group.activated = group_data.get('activated') + + if 'config' in group_data and group_data['config']: + group.config = [] + for config_data in group_data['config']: + group.config.append(_parse_config(config_data)) + + return group + +def _parse_config(config_data: dict) -> Config: + """ Parse config data from JSON """ + + config = Config() + config.key = config_data.get('key') + config.activated = config_data.get('activated') + + if 'strategies' in config_data and config_data['strategies']: + config.strategies = [] + for strategy_data in config_data['strategies']: + config.strategies.append(_parse_strategy(strategy_data)) + + if 'relay' in config_data and config_data['relay']: + config.relay = _parse_relay(config_data['relay']) + + return config + +def _parse_strategy(strategy_data: dict) -> StrategyConfig: + """ Parse strategy data from JSON """ + + strategy = StrategyConfig() + strategy.strategy = strategy_data.get('strategy') + strategy.activated = strategy_data.get('activated') + strategy.operation = strategy_data.get('operation') + strategy.values = strategy_data.get('values') + + return strategy + +def _parse_relay(relay_data: dict) -> Relay: + """ Parse relay data from JSON """ + + relay = Relay() + relay.type = relay_data.get('type') + relay.activated = relay_data.get('activated') + + return relay \ No newline at end of file diff --git a/switcher_client/lib/types.py b/switcher_client/lib/types.py index cb5679d..07f26c8 100644 --- a/switcher_client/lib/types.py +++ b/switcher_client/lib/types.py @@ -19,5 +19,29 @@ def __init__(self): self.name: Optional[str] = None self.version: int = 0 self.activated: Optional[bool] = None + self.group: Optional[List[Group]] = None +class Group: + def __init__(self): + self.name: Optional[str] = None + self.activated: Optional[bool] = None + self.config: Optional[List[Config]] = None + +class Config: + def __init__(self): + self.key: Optional[str] = None + self.activated: Optional[bool] = None + self.strategies: Optional[List[StrategyConfig]] = None + self.relay: Optional[Relay] = None +class StrategyConfig: + def __init__(self): + self.strategy: Optional[str] = None + self.activated: Optional[bool] = None + self.operation: Optional[str] = None + self.values: Optional[List[str]] = None + +class Relay: + def __init__(self): + self.type: Optional[str] = None + self.activated: Optional[bool] = None \ No newline at end of file diff --git a/tests/snapshots/default.json b/tests/snapshots/default.json new file mode 100644 index 0000000..e295ea8 --- /dev/null +++ b/tests/snapshots/default.json @@ -0,0 +1,162 @@ +{ + "data": { + "domain": { + "name": "Business", + "description": "Business description", + "activated": true, + "version": 1, + "group": [ + { + "name": "Rollout 2020", + "description": "Changes that will be applied during the rollout", + "activated": true, + "config": [ + { + "key": "FF2FOR2020", + "description": "Feature Flag", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "10.0.0.3/24" + ] + }, + { + "strategy": "VALUE_VALIDATION", + "activated": true, + "operation": "NOT_EXIST", + "values": [ + "USA", + "Canada", + "Australia", + "Africa" + ] + } + ], + "components": [] + }, + { + "key": "FF2FOR2021", + "description": "Strategy disabled", + "activated": true, + "strategies": [ + { + "strategy": "NETWORK_VALIDATION", + "activated": false, + "operation": "EXIST", + "values": [ + "10.0.0.3/24" + ] + } + ], + "components": [] + }, + { + "key": "FF2FOR2022", + "description": "No strategies", + "activated": true, + "components": [] + }, + { + "key": "FF2FOR2023", + "description": "Feature Flag - Payload Strategy", + "activated": true, + "strategies": [ + { + "strategy": "PAYLOAD_VALIDATION", + "activated": true, + "operation": "HAS_ALL", + "values": [ + "id", "user", "user.login", "user.role" + ] + } + ], + "components": [] + }, + { + "key": "FF2FOR2024", + "description": "reDOS safe test", + "activated": true, + "strategies": [ + { + "strategy": "REGEX_VALIDATION", + "activated": true, + "operation": "EXIST", + "values": [ + "^(([a-z])+.)+[A-Z]([a-z])+$" + ] + } + ], + "components": [] + } + ] + }, + { + "name": "Rollout 2030", + "description": "Changes that will be applied during the rollout", + "activated": true, + "config": [ + { + "key": "FF2FOR2030", + "description": "Feature Flag", + "activated": true, + "strategies": [], + "components": [] + }, + { + "key": "FF2FOR2031", + "description": "Feature Flag disabled", + "activated": false, + "strategies": [], + "components": [] + } + ] + }, + { + "name": "Rollout 2040", + "description": "Project is disabled", + "activated": false, + "config": [ + { + "key": "FF2FOR2040", + "description": "Feature Flag", + "activated": true, + "strategies": [], + "components": [] + } + ] + }, + { + "name": "Relay test", + "description": "Relay group", + "activated": true, + "config": [ + { + "key": "USECASE103", + "description": "Relay enabled", + "activated": true, + "relay": { + "type": "VALIDATOR", + "activated": true + }, + "components": [] + }, + { + "key": "USECASE104", + "description": "Relay disabled", + "relay": { + "type": "VALIDATOR", + "activated": false + }, + "activated": true, + "components": [] + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/test_client_context.py b/tests/test_client_context.py index b477b4d..f610f1f 100644 --- a/tests/test_client_context.py +++ b/tests/test_client_context.py @@ -1,3 +1,4 @@ +import os import pytest from switcher_client import Client, ContextOptions @@ -44,14 +45,14 @@ def test_context_with_optionals(): domain='My Domain', options=ContextOptions( local=True, - snapshot_location='./snapshots' + snapshot_location='./tests/snapshots' ) ) options = Client.context.options assert options.local == True - assert options.snapshot_location == './snapshots' + assert options.snapshot_location == './tests/snapshots' def test_load_from_snapshot(): """ Should load Domain from snapshot file """ @@ -60,7 +61,7 @@ def test_load_from_snapshot(): domain='My Domain', options=ContextOptions( local=True, - snapshot_location='./snapshots' + snapshot_location='./tests/snapshots' ) ) @@ -70,4 +71,34 @@ def test_load_from_snapshot(): # test version = Client.load_snapshot() assert Client.snapshot_version() == 1 - assert version == Client.snapshot_version() \ No newline at end of file + assert version == Client.snapshot_version() + +def test_load_from_snapshot_empty(): + """ Should create clean snapshot when no snapshot file exists """ + + Client.build_context( + domain='My Domain', + environment='generated-clean', + options=ContextOptions( + local=True, + snapshot_location='./tests/snapshots' + ) + ) + + # verify initial snapshot version + assert Client.snapshot_version() == 0 + + # test + version = Client.load_snapshot() + assert Client.snapshot_version() == 0 + assert version == Client.snapshot_version() + + # tear down + delete_snapshot_file('./tests/snapshots', 'generated-clean') + +# Helpers + +def delete_snapshot_file(snapshot_location: str, environment: str): + snapshot_file = f"{snapshot_location}/{environment}.json" + if os.path.exists(snapshot_file): + os.remove(snapshot_file) \ No newline at end of file