Skip to content

add a JSON schema for the VDOM spec #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/ambv/black
rev: stable
rev: 20.8b1
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ build

# VS Code RST extension builds here by default
source/_build
source/vdom-json-schema.json
17 changes: 17 additions & 0 deletions docs/source/_exts/copy_vdom_json_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import json
from pathlib import Path

from sphinx.application import Sphinx

from idom.core.vdom import SERIALIZED_VDOM_JSON_SCHEMA


def setup(app: Sphinx) -> None:
schema_file = Path(__file__).parent.parent / "vdom-json-schema.json"
current_schema = json.dumps(SERIALIZED_VDOM_JSON_SCHEMA, indent=2, sort_keys=True)

# We need to make this check because the autoreload system for the docs checks
# to see if the file has changed to determine whether to re-build. Thus we should
# only write to the file if its contents will be different.
if not schema_file.exists() or schema_file.read_text() != current_schema:
schema_file.write_text(current_schema)
6 changes: 4 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
"sphinx.ext.napoleon",
"sphinx.ext.autosectionlabel",
"sphinx_autodoc_typehints",
"sphinx_panels",
"sphinx_copybutton",
# custom extensions
"interactive_widget",
"widget_example",
"async_doctest",
"sphinx_panels",
"sphinx_copybutton",
"copy_vdom_json_schema",
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
5 changes: 2 additions & 3 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Libraries for defining and controlling interactive webpages with Python
life-cycle-hooks
core-concepts
javascript-components

specifications
package-api
examples

.. toctree::
Expand All @@ -21,8 +22,6 @@ Libraries for defining and controlling interactive webpages with Python

contributing
developer-guide
specifications
package-api

.. toctree::
:hidden:
Expand Down
3 changes: 0 additions & 3 deletions docs/source/mimetype.json

This file was deleted.

3 changes: 1 addition & 2 deletions docs/source/specifications.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,10 @@ type name. The various properties for the ``onChange`` handler are:

To clearly describe the VDOM schema we've created a `JSON Schema <https://json-schema.org/>`_:

.. literalinclude:: ./mimetype.json
.. literalinclude:: ./vdom-json-schema.json
:language: json



JSON Patch
----------

Expand Down
16 changes: 16 additions & 0 deletions idom/core/layout.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import abc
import asyncio
from functools import wraps
from typing import (
Any,
AsyncIterator,
Expand All @@ -17,10 +18,13 @@
from jsonpatch import apply_patch, make_patch
from loguru import logger

from idom.options import IDOM_DEBUG

from .component import AbstractComponent
from .events import EventHandler, EventTarget
from .hooks import LifeCycleHook
from .utils import CannotAccessResource, HasAsyncResources, async_resource
from .vdom import validate_serialized_vdom


class LayoutUpdate(NamedTuple):
Expand Down Expand Up @@ -92,6 +96,18 @@ async def render(self) -> LayoutUpdate:
if self._has_component_state(component):
return self._create_layout_update(component)

if IDOM_DEBUG:
from loguru import logger

_debug_render = render

@wraps(_debug_render)
async def render(self) -> LayoutUpdate:
# Ensure that the model is valid VDOM on each render
result = await self._debug_render()
validate_serialized_vdom(self._component_states[id(self.root)].model)
return result

@async_resource
async def _rendering_queue(self) -> AsyncIterator["_ComponentQueue"]:
queue = _ComponentQueue()
Expand Down
58 changes: 58 additions & 0 deletions idom/core/vdom.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,69 @@
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Union

from fastjsonschema import compile as compile_json_schema
from mypy_extensions import TypedDict
from typing_extensions import Protocol

from .component import AbstractComponent
from .events import EventsMapping

SERIALIZED_VDOM_JSON_SCHEMA = {
"$schema": "http://json-schema.org/draft-07/schema",
"$ref": "#/definitions/element",
"definitions": {
"element": {
"type": "object",
"properties": {
"tagName": {"type": "string"},
"children": {"$ref": "#/definitions/elementChildren"},
"attributes": {"type": "object"},
"eventHandlers": {"$ref": "#/definitions/elementEventHandlers"},
"importSource": {"$ref": "#/definitions/importSource"},
},
"required": ["tagName"],
},
"elementChildren": {
"type": "array",
"items": {"$ref": "#/definitions/elementOrString"},
},
"elementEventHandlers": {
"type": "object",
"patternProperties": {
".*": {"$ref": "#/definitions/eventHander"},
},
},
"eventHander": {
"type": "object",
"properties": {
"target": {"type": "string"},
"preventDefault": {"type": "boolean"},
"stopPropagation": {"type": "boolean"},
},
"required": ["target"],
},
"importSource": {
"type": "object",
"properties": {
"source": {"type": "string"},
"fallback": {
"type": ["object", "string", "null"],
"if": {"not": {"type": "null"}},
"then": {"$ref": "#/definitions/elementOrString"},
},
},
"required": ["source"],
},
"elementOrString": {
"type": ["object", "string"],
"if": {"type": "object"},
"then": {"$ref": "#/definitions/element"},
},
},
}


validate_serialized_vdom = compile_json_schema(SERIALIZED_VDOM_JSON_SCHEMA)


class ImportSourceDict(TypedDict):
source: str
Expand Down
25 changes: 25 additions & 0 deletions idom/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Any, Callable, Dict, Type

IDOM_DEBUG = False


def _init() -> None:
"""Collect options from :attr:`os.environ`"""
import os

from_string: Dict[Type[Any], Callable[[Any], Any]] = {
bool: lambda v: bool(int(v)),
}

module = globals()
for name, default in globals().items():
value_type = type(default)
value = os.environ.get(name, default)
if value_type in from_string:
value = from_string[value_type](value)
module[name] = value

return None


_init()
1 change: 1 addition & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def test(session: Session) -> None:
@nox.session
def test_python(session: Session) -> None:
"""Run the Python-based test suite"""
session.env["IDOM_DEBUG"] = "1"
session.install("-r", "requirements/test-env.txt")
session.install(".[all]")
args = ["pytest", "tests"] + get_posargs("pytest", session)
Expand Down
2 changes: 1 addition & 1 deletion requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ async_exit_stack >=1.0.1; python_version<"3.7"
jsonpatch >=1.26
typer >=0.3.2
click-spinner >=0.1.10
jsonschema >=3.2.0
fastjsonschema >=2.14.5
Loading