From 812b415a6995e410d0cca3be81dec91cc7fd5d51 Mon Sep 17 00:00:00 2001
From: Garrick Aden-Buie <garrick@adenbuie.com>
Date: Thu, 19 Sep 2024 22:34:38 -0400
Subject: [PATCH] feat(typography): Easy font setting, default is google font

Treats `base: Open Sans` and
`base.family: Open Sans` as "get from Google Fonts"

For #21
---
 examples/brand-typography-minimal.yml         |  9 +++
 pkg-py/src/brand_yaml/typography.py           | 55 ++++++++++++++++++-
 .../test_brand_typography_ex_color.json       |  1 +
 .../test_brand_typography_ex_minimal.json     | 38 +++++++++++++
 .../test_brand_typography_ex_simple.json      | 14 +++++
 pkg-py/tests/test_typography.py               | 30 +++++++++-
 6 files changed, 145 insertions(+), 2 deletions(-)
 create mode 100644 examples/brand-typography-minimal.yml
 create mode 100644 pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_minimal.json

diff --git a/examples/brand-typography-minimal.yml b/examples/brand-typography-minimal.yml
new file mode 100644
index 00000000..8ff78f73
--- /dev/null
+++ b/examples/brand-typography-minimal.yml
@@ -0,0 +1,9 @@
+meta:
+  name: examples/brand-typography-simple.yml
+typography:
+  fonts:
+    - family: Open Sans
+      source: file
+  base: Open Sans
+  headings: Roboto Slab
+  monospace: Fira Code
\ No newline at end of file
diff --git a/pkg-py/src/brand_yaml/typography.py b/pkg-py/src/brand_yaml/typography.py
index d4e75e97..2561c942 100644
--- a/pkg-py/src/brand_yaml/typography.py
+++ b/pkg-py/src/brand_yaml/typography.py
@@ -146,7 +146,7 @@ class BrandTypographyFontFiles(BaseModel):
 
     source: Literal["file"] = "file"
     family: str
-    files: list[BrandTypographyFontFilesPath]
+    files: list[BrandTypographyFontFilesPath] = Field(default_factory=list)
 
 
 class BrandTypographyFontFilesPath(BaseModel):
@@ -413,6 +413,59 @@ class BrandTypography(BrandBase):
     )
     link: BrandTypographyLink | None = None
 
+    @model_validator(mode="before")
+    @classmethod
+    def simple_google_fonts(cls, data: Any):
+        if not isinstance(data, dict):
+            return data
+
+        defined_families = set()
+        file_families = set()
+
+        if (
+            "fonts" in data
+            and isinstance(data["fonts"], list)
+            and len(data["fonts"]) > 0
+        ):
+            for font in data["fonts"]:
+                defined_families.add(font["family"])
+                if font["source"] == "file":
+                    file_families.add(font["family"])
+        else:
+            data["fonts"] = []
+
+        for field in (
+            "base",
+            "headings",
+            "monospace",
+            "monospace_inline",
+            "monospace_block",
+        ):
+            if field not in data:
+                continue
+
+            if not isinstance(data[field], (str, dict)):
+                continue
+
+            if isinstance(data[field], str):
+                data[field] = {"family": data[field]}
+
+            if "family" not in data[field]:
+                continue
+
+            if data[field]["family"] in defined_families:
+                continue
+
+            data["fonts"].append(
+                {
+                    "family": data[field]["family"],
+                    "source": "google",
+                }
+            )
+            defined_families.add(data[field]["family"])
+
+        return data
+
     @model_validator(mode="after")
     def forward_monospace_values(self):
         """
diff --git a/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_color.json b/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_color.json
index 1c74f306..ba1b90f7 100644
--- a/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_color.json
+++ b/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_color.json
@@ -18,6 +18,7 @@
     "base": {
       "color": "#1b1818"
     },
+    "fonts": [],
     "headings": {
       "color": "#87CEEB"
     },
diff --git a/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_minimal.json b/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_minimal.json
new file mode 100644
index 00000000..1849fccb
--- /dev/null
+++ b/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_minimal.json
@@ -0,0 +1,38 @@
+{
+  "meta": {
+    "name": {
+      "full": "examples/brand-typography-simple.yml"
+    }
+  },
+  "typography": {
+    "base": {
+      "family": "Open Sans"
+    },
+    "fonts": [
+      {
+        "family": "Open Sans",
+        "source": "file"
+      },
+      {
+        "family": "Roboto Slab",
+        "source": "google"
+      },
+      {
+        "family": "Fira Code",
+        "source": "google"
+      }
+    ],
+    "headings": {
+      "family": "Roboto Slab"
+    },
+    "monospace": {
+      "family": "Fira Code"
+    },
+    "monospace-block": {
+      "family": "Fira Code"
+    },
+    "monospace-inline": {
+      "family": "Fira Code"
+    }
+  }
+}
diff --git a/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_simple.json b/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_simple.json
index 5523c38f..492aa61e 100644
--- a/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_simple.json
+++ b/pkg-py/tests/__snapshots__/test_typography/test_brand_typography_ex_simple.json
@@ -10,6 +10,20 @@
       "line-height": 1.25,
       "size": "1rem"
     },
+    "fonts": [
+      {
+        "family": "Open Sans",
+        "source": "google"
+      },
+      {
+        "family": "Roboto Slab",
+        "source": "google"
+      },
+      {
+        "family": "Fira Code",
+        "source": "google"
+      }
+    ],
     "headings": {
       "color": "primary",
       "family": "Roboto Slab",
diff --git a/pkg-py/tests/test_typography.py b/pkg-py/tests/test_typography.py
index d769163b..659074a5 100644
--- a/pkg-py/tests/test_typography.py
+++ b/pkg-py/tests/test_typography.py
@@ -258,7 +258,16 @@ def test_brand_typography_ex_simple(snapshot_json):
     brand = read_brand_yaml(path_examples("brand-typography-simple.yml"))
 
     assert isinstance(brand.typography, BrandTypography)
-    assert brand.typography.fonts == []
+
+    assert isinstance(brand.typography.fonts, list)
+    assert len(brand.typography.fonts) == 3
+    assert [f.family for f in brand.typography.fonts] == [
+        "Open Sans",
+        "Roboto Slab",
+        "Fira Code",
+    ]
+    assert [f.source for f in brand.typography.fonts] == ["google"] * 3
+
     assert brand.typography.link is None
     assert isinstance(brand.typography.base, BrandTypographyBase)
     assert isinstance(brand.typography.headings, BrandTypographyHeadings)
@@ -355,3 +364,22 @@ def test_brand_typography_ex_color(snapshot_json):
     assert t.link.color == color.palette["red"]
 
     assert snapshot_json == pydantic_data_from_json(brand)
+
+
+def test_brand_typography_ex_minimal(snapshot_json):
+    brand = read_brand_yaml(path_examples("brand-typography-minimal.yml"))
+
+    assert isinstance(brand.typography, BrandTypography)
+
+    assert isinstance(brand.typography.fonts, list)
+    assert len(brand.typography.fonts) == 3
+    assert brand.typography.fonts[0].source == "file"
+    assert brand.typography.fonts[0].files == []
+
+    assert isinstance(brand.typography.fonts[1], BrandTypographyFontGoogle)
+    assert brand.typography.fonts[1].family == "Roboto Slab"
+
+    assert isinstance(brand.typography.fonts[2], BrandTypographyFontGoogle)
+    assert brand.typography.fonts[2].family == "Fira Code"
+
+    assert snapshot_json == pydantic_data_from_json(brand)