+{{ escaped_sources }}
+
+ -
+ {% for diagram in mmd_diagrams %}
+
- {{ diagram.name }} + {% endfor %} +
diff --git a/pyproject.toml b/pyproject.toml index baa118d..d2d5517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,10 @@ license = "LGPL-3.0-or-later" requires-python = ">=3.13" dependencies = [ "megasniff>=0.2.0", - "breakshaft>=0.1.1", + "breakshaft>=0.1.3", "case-insensitive-dictionary>=0.2.1", "mypy>=1.17.0", + "jinja2>=3.1.6", ] [tool.uv.sources] @@ -46,4 +47,4 @@ all_containers = [ { "include-group" = "xml" }, { "include-group" = "msgpack" }, { "include-group" = "json" }, -] \ No newline at end of file +] diff --git a/src/turbosloth/__main__.py b/src/turbosloth/__main__.py index 12f7833..6c0c88c 100644 --- a/src/turbosloth/__main__.py +++ b/src/turbosloth/__main__.py @@ -10,7 +10,7 @@ from turbosloth.interfaces.serialized import SerializedResponse, SerializedReque from turbosloth.internal_types import QTYPE, BTYPE, PTYPE, HTYPE from turbosloth.req_schema import UnwrappedRequest -app = SlothApp() +app = SlothApp(di_autodoc_prefix='/didoc') @app.get("/") diff --git a/src/turbosloth/app.py b/src/turbosloth/app.py index 8573a16..2175ddf 100644 --- a/src/turbosloth/app.py +++ b/src/turbosloth/app.py @@ -1,11 +1,13 @@ +import html import typing from typing import Optional, Callable, Awaitable, Protocol, get_type_hints, get_origin, get_args, Any, Annotated import breakshaft.util_mermaid import megasniff.exceptions -from breakshaft.models import ConversionPoint +from breakshaft.models import ConversionPoint, Callgraph from megasniff import SchemaInflatorGenerator +from .didoc import create_di_autodoc_handler from .exceptions import HTTPException from .interfaces.base import BasicRequest, BasicResponse from .interfaces.serialize_selector import SerializeSelector @@ -179,16 +181,24 @@ class MethodRoutersApp(ASGIApp): class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp): + di_autodoc_prefix: Optional[str] = None def __init__(self, on_startup: Optional[Callable[[], Awaitable[None]]] = None, - on_shutdown: Optional[Callable[[], Awaitable[None]]] = None): + on_shutdown: Optional[Callable[[], Awaitable[None]]] = None, + di_autodoc_prefix: Optional[str] = None, + ): self.router = Router() self._on_startup = on_startup self._on_shutdown = on_shutdown self.serialize_selector = SerializeSelector() self.infl_generator = SchemaInflatorGenerator(strict_mode=True) - self.inj_repo = ConvRepo() + self.di_autodoc_prefix = di_autodoc_prefix + + if di_autodoc_prefix is not None: + self.inj_repo = ConvRepo(store_sources=True, store_callseq=True) + else: + self.inj_repo = ConvRepo() @self.inj_repo.mark_injector() def extract_query(req: BasicRequest | SerializedRequest) -> QTYPE: @@ -231,42 +241,43 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp): if get_origin(tp) == UnwrappedRequest: req_schema = tp - if req_schema is None: - raise ValueError(f'Unable to find request schema in handler {fn}') + if req_schema is not None: - unwrap_types = get_args(req_schema) - defaults = (QTYPE, BTYPE, PTYPE, HTYPE) + unwrap_types = get_args(req_schema) + defaults = (QTYPE, BTYPE, PTYPE, HTYPE) - def none_generator(*args) -> None: - return None + def none_generator(*args) -> None: + return None - def create_convertor(t, def_type): - infl = None - if t not in [def_type, None, Any]: - infl = self.infl_generator.schema_to_inflator( - t, - strict_mode_override=False, - from_type_override=def_type + def create_convertor(t, def_type): + infl = None + if t not in [def_type, None, Any]: + infl = self.infl_generator.schema_to_inflator( + t, + strict_mode_override=False, + from_type_override=def_type + ) + return ConversionPoint(infl, t, (def_type,), (), ) + else: + return ConversionPoint(none_generator, t, (def_type,), (), ) + + fork_with = set(map(lambda x: create_convertor(*x), zip(unwrap_types, defaults))) + + def construct_unwrap(q: QTYPE, b: BTYPE, p: PTYPE, h: HTYPE) -> UnwrappedRequest: + return UnwrappedRequest(q, b, p, h) + + fork_with |= { + ConversionPoint( + construct_unwrap, + req_schema, + unwrap_types, + (), ) - return ConversionPoint(infl, t, (def_type,), (), ) - else: - return ConversionPoint(none_generator, t, (def_type,), (), ) + } - fork_with = set(map(lambda x: create_convertor(*x), zip(unwrap_types, defaults))) - - def construct_unwrap(q: QTYPE, b: BTYPE, p: PTYPE, h: HTYPE) -> UnwrappedRequest: - return UnwrappedRequest(q, b, p, h) - - fork_with |= { - ConversionPoint( - construct_unwrap, - req_schema, - unwrap_types, - (), - ) - } - - tmp_repo = self.inj_repo.fork(fork_with) + tmp_repo = self.inj_repo.fork(fork_with) + else: + tmp_repo = self.inj_repo p = tmp_repo.create_pipeline( (Send, BasicRequest), @@ -275,6 +286,12 @@ class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp): ) self.router.add(method, path_pattern, p) + + if self.di_autodoc_prefix is not None and not path_pattern.startswith( + self.di_autodoc_prefix + '/' + method + self.di_autodoc_prefix): + self.route('GET', self.di_autodoc_prefix + '/' + method + path_pattern)( + create_di_autodoc_handler(method, path_pattern, p)) + return fn return decorator diff --git a/src/turbosloth/didoc/__init__.py b/src/turbosloth/didoc/__init__.py new file mode 100644 index 0000000..7897137 --- /dev/null +++ b/src/turbosloth/didoc/__init__.py @@ -0,0 +1,50 @@ +import html +import importlib.resources +from dataclasses import dataclass +from typing import Callable + +import breakshaft.util_mermaid +import jinja2 + +from turbosloth.interfaces.serialized import SerializedResponse +from turbosloth.interfaces.serialized.html import HTMLSerializedResponse +from turbosloth.types import InternalHandlerType + + +@dataclass +class MMDiagramData: + name: str + data: str + + +def create_di_autodoc_handler(method: str, path: str, handler: InternalHandlerType) -> Callable[[], SerializedResponse]: + callseq = getattr(handler, '__breakshaft_callseq__', []) + mmd_flowchart = breakshaft.util_mermaid.draw_callseq_mermaid(callseq) + mmd_flowchart1 = breakshaft.util_mermaid.draw_callseq_mermaid([callseq[0]] + callseq) + mmd_flowchart2 = breakshaft.util_mermaid.draw_callseq_mermaid([callseq[-1]] + callseq) + + sources = getattr(handler, '__breakshaft_render_src__', '') + escaped_sources = html.escape(sources) + + template_path = importlib.resources.files('turbosloth.didoc') + loader = jinja2.FileSystemLoader(str(template_path)) + templateEnv = jinja2.Environment(loader=loader) + template = templateEnv.get_template('page.jinja2') + + mmd_diagrams = [ + MMDiagramData('1', html.escape(mmd_flowchart)), + MMDiagramData('2', html.escape(mmd_flowchart1)), + MMDiagramData('3', html.escape(mmd_flowchart2)), + ] + + html_content = template.render( + handler_method=method, + handler_path=path, + escaped_sources=escaped_sources, + mmd_diagrams=mmd_diagrams, + ) + + def _h() -> SerializedResponse: + return HTMLSerializedResponse(200, {}, html_content) + + return _h diff --git a/src/turbosloth/didoc/page.jinja2 b/src/turbosloth/didoc/page.jinja2 new file mode 100644 index 0000000..56c8781 --- /dev/null +++ b/src/turbosloth/didoc/page.jinja2 @@ -0,0 +1,244 @@ + + +
+ +
+{{ escaped_sources }}
+
+