diff --git a/antigen/__init__.py b/antigen/__init__.py
index e65784e..2b0fc29 100644
--- a/antigen/__init__.py
+++ b/antigen/__init__.py
@@ -1 +1,4 @@
-__version__ = 0.0.1
\ No newline at end of file
+__version__ = "0.0.1"
+
+from .blueprint import *
+from .utils import *
diff --git a/antigen/blueprint.py b/antigen/blueprint.py
new file mode 100644
index 0000000..c9a387f
--- /dev/null
+++ b/antigen/blueprint.py
@@ -0,0 +1,72 @@
+import glob
+import os
+
+from jinja2 import Environment, FileSystemLoader, PackageLoader, Template
+
+from antigen import components_dir
+
+from .utils import files_in_folder, stdout_logger
+
+log = stdout_logger(__name__)
+
+jinja_env = Environment(loader=FileSystemLoader(components_dir))
+
+###############################################################################
+## Blueprint
+###############################################################################
+
+
+def component_is_valid(component):
+
+ if "component" not in component:
+ log.warning("component field missing in definition")
+ return False
+
+ fields = ["component", "id", "props", "children"]
+ for f in fields:
+ if f not in component:
+ log.warning(f"{f} missing in component: {component['component']}")
+ return False
+ return True
+
+
+def create_component(component: dict, path: str, templates: str) -> str:
+ """
+ Args:
+ component: component definition ('component', 'id', 'props', 'children' keys are used in generation)
+ path: path to create component js file
+ templates: path to template folder
+
+ Returns:
+ str: path to component js file
+ """
+
+ if os.path.isfile(path):
+ raise Exception(f"{path} already exists")
+
+ if not component_is_valid(component):
+ raise Exception(f"invalid component")
+
+ templates = jinja_env.list_templates()
+ log.debug(templates)
+
+ if f"{component['component']}.js" in templates:
+ template = f"{component['component']}.js"
+ elif "Default.js" in templates:
+ log.debug(f"no template found for f{component['component']} using Default.js")
+ template = "Default.js"
+ else:
+ raise Exception(f"no component templates found for {component['component']}")
+
+ with open(path, "w") as f:
+ log.debug(f"Generating template {template} -> {path} ...")
+ f.write(
+ jinja_env.get_template(template).render(
+ id=component["id"],
+ component=component["component"],
+ children=component["children"],
+ props=component["props"],
+ )
+ )
+
+ return path
diff --git a/antigen/resources/components/Default.js b/antigen/resources/components/Default.js
new file mode 100644
index 0000000..6728cd6
--- /dev/null
+++ b/antigen/resources/components/Default.js
@@ -0,0 +1,6 @@
+function {{component}}(props) {
+
+ return (
+
{{children}}
+ )
+}
\ No newline at end of file
diff --git a/antigen/resources/examples/blueprints/simple_blueprint.yaml b/antigen/resources/examples/blueprints/simple_blueprint.yaml
new file mode 100644
index 0000000..f4a5a05
--- /dev/null
+++ b/antigen/resources/examples/blueprints/simple_blueprint.yaml
@@ -0,0 +1,7 @@
+components:
+
+ - component: TextBox
+ props:
+ id: standard-basic
+ label: Standard
+ children: Hello World!
\ No newline at end of file
diff --git a/antigen/resources/templates/README.md b/antigen/resources/templates/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/antigen/utils.py b/antigen/utils.py
new file mode 100644
index 0000000..2d6d7d0
--- /dev/null
+++ b/antigen/utils.py
@@ -0,0 +1,78 @@
+import logging
+import os
+import shutil
+import sys
+
+import yaml
+
+###############################################################################
+## Important Paths
+###############################################################################
+
+module_dir = os.path.dirname(sys.modules["antigen"].__file__)
+resources_dir = os.path.join(module_dir, "resources")
+components_dir = os.path.join(resources_dir, "components")
+examples_dir = os.path.join(resources_dir, "examples")
+
+###############################################################################
+## General
+###############################################################################
+
+
+def read_yaml(file):
+ with open(file, "r") as f:
+ return yaml.load(f, Loader=yaml.FullLoader)
+
+
+def reverse_dictionary(d):
+ return {v: k for k, v in d.items()}
+
+
+def delete_directory(directory):
+ if os.path.isdir(directory):
+ shutil.rmtree(directory)
+
+
+def make_directory(directory, delete=False, exist_ok=True):
+ if delete:
+ delete_directory(directory)
+ if not os.path.isdir(directory):
+ os.makedirs(directory, exist_ok=exist_ok)
+ return os.path.abspath(directory)
+
+
+def files_in_folder(folder):
+ return [
+ os.path.join(folder, n)
+ for n in os.listdir(folder)
+ if os.path.isfile(os.path.join(folder, n))
+ ]
+
+
+def folders_in_folder(folder):
+ return [
+ os.path.join(folder, n)
+ for n in os.listdir(folder)
+ if os.path.isdir(os.path.join(folder, n))
+ ]
+
+
+def stdout_logger(name):
+ """
+ Use this logger to standardize log output
+ """
+ log = logging.getLogger(name)
+ log.propagate = False
+ stream_handler = logging.StreamHandler(sys.stdout)
+ formatter = logging.Formatter("%(levelname)-8s %(message)s")
+ stream_handler.setFormatter(formatter)
+ stream_handler.setLevel(logging.DEBUG)
+ log.handlers = [stream_handler]
+ log.setLevel(logging.DEBUG)
+ return log
+
+
+class class_or_instance_method(classmethod):
+ def __get__(self, instance, type_):
+ descr_get = super().__get__ if instance is None else self.__func__.__get__
+ return descr_get(instance, type_)
diff --git a/bin/antigen b/bin/antigen
index e69de29..a1689c0 100755
--- a/bin/antigen
+++ b/bin/antigen
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+import argparse
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+from glob import glob
+from importlib import import_module
+
+from jinja2 import Environment, FileSystemLoader, PackageLoader, Template
+
+from antigen import folders_in_folder, read_yaml, stdout_logger
+
+log = stdout_logger(__name__)
+module_folder = os.path.dirname(sys.modules["antigen"].__file__)
+resources_folder = os.path.join(module_folder, "resources")
+current_folder = os.path.abspath(os.curdir)
+
+templates = {
+ os.path.basename(t): t
+ for t in folders_in_folder(os.path.join(module_folder, "resources", "templates"))
+}
+
+# parsers
+parser = argparse.ArgumentParser(description="declarative ui generation")
+subparsers = parser.add_subparsers(dest="subcommand")
+
+# parsers - blueprint
+generate_parser = subparsers.add_parser(
+ "blueprint", description="generate components from blueprint"
+)
+generate_parser.add_argument("path", help="path to blueprint YAML")
+
+args = parser.parse_args()
+
+try:
+
+ if args.subcommand == "blueprint":
+ log.debug(read_yaml(args.path))
+
+ else:
+ parser.print_help()
+
+except Exception as e:
+ log.error(e)
+ parser.print_help()
+
diff --git a/setup.py b/setup.py
index 6e40a14..ef170c6 100644
--- a/setup.py
+++ b/setup.py
@@ -1,14 +1,21 @@
from distutils.core import setup
-from antigen import __version__
+
from setuptools import find_packages
+from antigen import __version__
+
setup(
name="antigen",
version=__version__,
author="shirecoding",
author_email="shirecoding@gmail.com",
scripts=["bin/antigen"],
- install_requires=["Jinja2"],
+ install_requires=[
+ "Jinja2",
+ "pytest",
+ "pytest-html",
+ "pytest-cov",
+ ],
url="https://github.com/shirecoding/Antigen",
download_url=f"https://github.com/shirecoding/Antigen/archive/{__version__}.tar.gz",
long_description=open("README.md").read(),
diff --git a/tests/.coveragerc b/tests/.coveragerc
new file mode 100644
index 0000000..d05a577
--- /dev/null
+++ b/tests/.coveragerc
@@ -0,0 +1,2 @@
+[run]
+data_file = reports/.coverage
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..0f7cb2e
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,52 @@
+from datetime import datetime
+
+import pytest
+from py.xml import html
+
+###########################################################################################
+## REPORT
+###########################################################################################
+
+
+def stringToCellData(string, class_):
+ return html.td([html.p(s) for s in string.split("\n")], class_=class_)
+
+
+@pytest.mark.optionalhook
+def pytest_html_results_table_header(cells):
+ cells.insert(1, html.th("Specification", class_="spec", col="spec"))
+ cells.insert(2, html.th("Description"))
+ cells.insert(3, html.th("Procedure", class_="proc", col="proc"))
+ cells.insert(4, html.th("Expected", class_="expec", col="expec"))
+ cells.pop() # remove links column
+
+
+@pytest.mark.optionalhook
+def pytest_html_results_table_row(report, cells):
+ cells.insert(1, stringToCellData(report.specification, class_="col-spec"))
+ cells.insert(2, stringToCellData(report.description, class_="col-desc"))
+ cells.insert(3, stringToCellData(report.procedure, class_="col-proc"))
+ cells.insert(4, stringToCellData(report.expected, class_="col-expec"))
+ cells.pop() # remove links column
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_makereport(item, call):
+ outcome = yield
+ report = outcome.get_result()
+ report.description = str(item.function.__doc__ if item.function.__doc__ else "")
+ marker = item.get_closest_marker("report")
+
+ report.specification = (
+ marker.kwargs["specification"]
+ if marker and "specification" in marker.kwargs
+ else ""
+ )
+
+ report.procedure = (
+ marker.kwargs["procedure"] if marker and "procedure" in marker.kwargs else ""
+ )
+
+ report.expected = (
+ marker.kwargs["expected"] if marker and "expected" in marker.kwargs else ""
+ )
diff --git a/tests/pytest.ini b/tests/pytest.ini
new file mode 100644
index 0000000..078f85c
--- /dev/null
+++ b/tests/pytest.ini
@@ -0,0 +1,14 @@
+[pytest]
+addopts =
+ --assert='plain'
+ --html=reports/pytest_report.html
+ --junitxml=reports/pytest_report.xml
+ --self-contained-html
+ --cov=antigen
+ --cov-report=xml:reports/coverage.xml
+ --cov-report=html:reports/coverage
+ --cov-config=tests/.coveragerc
+log_cli = true
+log_level = DEBUG
+markers =
+ report
\ No newline at end of file
diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py
new file mode 100644
index 0000000..ad57abf
--- /dev/null
+++ b/tests/test_blueprint.py
@@ -0,0 +1,70 @@
+import logging
+import os
+import re
+import tempfile
+
+import pytest
+
+log = logging.getLogger(__name__)
+
+
+@pytest.mark.report(
+ specification="""
+ """,
+ procedure="""
+ """,
+ expected="""
+ """,
+)
+def test_valid_component():
+ """
+ Test component validation function
+ """
+ from antigen import component_is_valid, components_dir, create_component
+
+ c1 = {
+ "component": "TextField",
+ "id": "TextField1",
+ "props": {},
+ "children": "Hello World",
+ }
+
+ assert component_is_valid(c1) == True
+
+ c2 = {"id": "TextField1", "props": {}, "children": "Hello World"}
+
+ assert component_is_valid(c2) == False
+
+
+@pytest.mark.report(
+ specification="""
+ """,
+ procedure="""
+ """,
+ expected="""
+ """,
+)
+def test_create_component():
+ """
+ Test component creation function
+ """
+
+ from antigen import components_dir, create_component
+
+ c1 = {
+ "component": "TextField",
+ "id": "TextField1",
+ "props": {},
+ "children": "Hello World",
+ }
+
+ with tempfile.TemporaryDirectory() as folder:
+ component_path = create_component(
+ c1, os.path.join(folder, "c1"), components_dir
+ )
+
+ with open(component_path, "r") as file:
+ data = file.read()
+ assert re.match(r"^.*id=\"TextField1\".*$", data, re.DOTALL) != None
+ assert re.match(r"^.*>Hello World<.*$", data, re.DOTALL) != None
+ assert re.match(r"^.*TextField\(props\).*$", data, re.DOTALL) != None