From bd8b7414b8c6e60c0d4dddfc107036131597353b Mon Sep 17 00:00:00 2001
From: Tony Narlock <tony@git-pull.com>
Date: Sun, 25 Sep 2022 17:48:34 -0500
Subject: [PATCH] feat(config): Config object

---
 src/vcspull/config.py       | 84 +++++++++++++++++++++++++++++++++++++
 tests/test_config_object.py | 51 ++++++++++++++++++++++
 2 files changed, 135 insertions(+)
 create mode 100644 tests/test_config_object.py

diff --git a/src/vcspull/config.py b/src/vcspull/config.py
index da1a7178..c65291fd 100644
--- a/src/vcspull/config.py
+++ b/src/vcspull/config.py
@@ -5,12 +5,17 @@
 A lot of these items are todo.
 
 """
+import dataclasses
 import fnmatch
 import logging
 import os
 import pathlib
 import typing as t
 
+import yaml
+
+from libvcs._internal.query_list import QueryList
+from libvcs._internal.shortcuts import create_project
 from libvcs.sync.git import GitRemote
 from vcspull.validator import is_valid_config
 
@@ -24,6 +29,85 @@
 if t.TYPE_CHECKING:
     from typing_extensions import TypeGuard
 
+    from libvcs.sync.git import GitSync
+    from libvcs.sync.hg import HgSync
+    from libvcs.sync.svn import SvnSync
+
+    Repo = t.Union["GitSync", "HgSync", "SvnSync"]
+
+
+@dataclasses.dataclass
+class Config:
+    repo_dict_map: list["Repo"]
+    repos: list["Repo"] = dataclasses.field(init=False, default_factory=list)
+
+    def __post_init__(self) -> None:
+        for repo in self.repo_dict_map:
+            if isinstance(repo, dict):
+                self.repos.append(create_project(**repo))
+
+    def filter_repos(self, **kwargs: object) -> list["Repo"]:
+        return QueryList(self.repos).filter(**kwargs)
+
+    @classmethod
+    def from_yaml_file(
+        cls, file_path: pathlib.Path, cwd: pathlib.Path = pathlib.Path.cwd()
+    ) -> "Config":
+        # load yaml
+        raw_config = yaml.load(open(file_path).read(), Loader=yaml.Loader)
+        repos: list[ConfigDict] = []
+        for directory, repo_map in raw_config.items():
+            assert isinstance(repo_map, dict)
+            for repo, repo_data in repo_map.items():
+                conf: dict[str, t.Any] = {}
+
+                if isinstance(repo_data, str):
+                    conf["url"] = repo_data
+                else:
+                    conf = update_dict(conf, repo_data)
+
+                if "repo" in conf:
+                    if "url" not in conf:
+                        conf["url"] = conf.pop("repo")
+                    else:
+                        conf.pop("repo", None)
+
+                if "name" not in conf:
+                    conf["name"] = repo
+
+                if "dir" not in conf:
+                    conf["dir"] = expand_dir(
+                        pathlib.Path(expand_dir(pathlib.Path(directory), cwd=cwd))
+                        / conf["name"],
+                        cwd,
+                    )
+
+                if "remotes" in conf:
+                    assert isinstance(conf["remotes"], dict)
+                    for remote_name, url in conf["remotes"].items():
+                        if isinstance(url, GitRemote):
+                            continue
+                        if isinstance(url, str):
+                            conf["remotes"][remote_name] = GitRemote(
+                                name=remote_name, fetch_url=url, push_url=url
+                            )
+                        elif isinstance(url, dict):
+                            assert "push_url" in url
+                            assert "fetch_url" in url
+                            conf["remotes"][remote_name] = GitRemote(
+                                name=remote_name, **url
+                            )
+
+                def is_valid_config_dict(val: t.Any) -> "TypeGuard[ConfigDict]":
+                    assert isinstance(val, dict)
+                    return True
+
+                assert is_valid_config_dict(conf)
+                repos.append(conf)
+
+        # assert validate_config(config)  # mypy happy
+        return cls(repo_dict_map=repos)  # type:ignore
+
 
 def expand_dir(
     _dir: pathlib.Path, cwd: pathlib.Path = pathlib.Path.cwd()
diff --git a/tests/test_config_object.py b/tests/test_config_object.py
new file mode 100644
index 00000000..1514cbb9
--- /dev/null
+++ b/tests/test_config_object.py
@@ -0,0 +1,51 @@
+import pathlib
+import typing as t
+
+import pytest
+
+from libvcs._internal.types import StrPath
+from vcspull import config as config_tools
+
+
+class LoadYaml(t.Protocol):
+    def __call__(
+        self, content: str, dir: StrPath = ..., filename: str = ...
+    ) -> pathlib.Path:
+        ...
+
+
+@pytest.fixture
+def load_yaml(tmp_path: pathlib.Path) -> LoadYaml:
+    def fn(
+        content: str, dir: StrPath = "randomdir", filename: str = "randomfilename.yaml"
+    ) -> pathlib.Path:
+        _dir = tmp_path / dir
+        _dir.mkdir()
+        _config = _dir / filename
+        _config.write_text(content, encoding="utf-8")
+
+        return _config
+
+    return fn
+
+
+def test_simple_format(load_yaml: LoadYaml) -> None:
+    config_file = load_yaml(
+        """
+vcspull:
+  libvcs: git+https://github.com/vcs-python/libvcs
+   """
+    )
+
+    config = config_tools.Config.from_yaml_file(config_file)
+
+    assert len(config.repos) == 1
+    repo = config.repos[0]
+    dir = repo.dir.parent.parent
+
+    assert dir / "vcspull" == repo.dir.parent
+    assert dir / "vcspull" / "libvcs" == repo.dir
+
+    assert hasattr(config, "filter_repos")
+    assert callable(config.filter_repos)
+    assert config.filter_repos(dir=dir / "vcspull" / "libvcs") == [repo]