[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/v0.43.1/src/pythonnative/sdk/codegen.py [Back]  [Original]

"""Generate component props decoders and portable native metadata."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from ..bridge.codec import to_jsonable
from .schema import COMPONENTS, fingerprint, manifest


def generate(destination: str | Path) -> list[Path]:
    """Write deterministic Swift, Kotlin, Python, and browser schema artifacts."""
    destination = Path(destination)
    destination.mkdir(parents=True, exist_ok=True)
    spec = manifest()
    encoded = json.dumps(spec, indent=2, sort_keys=True, default=str) + "\n"
    swift = ["import Foundation", "", "// Generated from Python schemas. Regenerate instead of editing."]
    kotlin = [
        "package com.pythonnative.generated",
        "",
        "import org.json.JSONObject",
        "",
        "// Generated from Python schemas.",
    ]
    python = [
        '"""Generated native component factories."""',
        "from typing import Any",
        "from pythonnative.element import Element",
        "from pythonnative.sdk.schema import COMPONENTS",
        "",
    ]
    docs = ["# Native contracts", "", f"Runtime fingerprint: `{fingerprint()}`.", ""]
    names = {
        "string": ("String", "String", "str"),
        "boolean": ("Bool", "Boolean", "bool"),
        "integer": ("Int", "Int", "int"),
        "number": ("Double", "Double", "float"),
    }
    for name, component in sorted(COMPONENTS.items()):
        if not name.isidentifier():
            raise ValueError(f"Invalid native component name {name!r}")
        swift.extend(
            [
                f"public struct {name}Props {{",
                "    public let values: [String: Any]",
                "    public init(_ values: [String: Any]) throws {",
                "        self.values = values",
            ]
        )
        kotlin.extend([f"data class {name}Props(val values: JSONObject) {{", "    init {"])
        parameters = []
        values = []
        for key, field in component.props.items():
            if not key.isidentifier():
                raise ValueError(f"Invalid property name {key!r}")
            kind = field.get("type")
            swift_type, kotlin_type, python_type = names.get(kind, ("Any", "Any", "Any"))
            if key in component.required:
                swift.append(
                    f'        guard values["{key}"] is {swift_type} else {{ '
                    f'throw NativeDecodeError.invalid("{name}.{key}") }}'
                )
                kotlin.append(
                    f'        require(values.has("{key}") && !values.isNull("{key}")) {{ "Missing {name}.{key}" }}'
                )
            default = component.defaults.get(key)
            if hasattr(default, "value"):
                default = default.value
            if default is None and python_type != "Any":
                python_type += " | None"
            parameters.append(f"{key}: {python_type}" + (f" = {default!r}" if key not in component.required else ""))
            values.append(f'"{key}": {key}')
        swift.append("    }")
        kotlin.append("    }")
        for key, field in component.props.items():
            kind = field.get("type")
            st, kt, _ = names.get(kind, ("Any", "Any", "Any"))
            expression = f'values["{key}"]' + (f" as? {st}" if st != "Any" else "")
            swift.append(f"    public var {key}: {st}? {{ {expression} }}")
            accessor = {"string": "getString", "boolean": "getBoolean", "integer": "getInt", "number": "getDouble"}.get(
                kind, "get"
            )
            kotlin.append(
                f'    val {key}: {kt}? get() = if (values.isNull("{key}")) null else values.{accessor}("{key}")'
            )
        swift.extend(["}", ""])
        kotlin.extend(["}", ""])
        python.extend(
            [
                f"def {name}(*children: Element, "
                + ", ".join(parameters + ["key: str | None = None"])
                + ") -> Element:",
                f'    """Create a validated {name} native element."""',
                "    props = {" + ", ".join(values) + "}",
                f'    COMPONENTS["{name}"].validate(props)',
                f'    return Element("{name}", props, list(children), key=key)',
                "",
            ]
        )
        docs.extend(
            [
                f"## {name}",
                "",
                f"Measurement: {component.measurement}.",
                "",
                "| Property | Type | Required |",
                "| --- | --- | --- |",
            ]
        )
        docs.extend(
            f"| `{key}` | `{field.get('type', 'union')}` | {key in component.required} |"
            for key, field in component.props.items()
        )
        docs.append("")
    swift.extend(["public enum NativeDecodeError: Error { case invalid(String) }", ""])
    outputs: dict[str, Any] = {
        "schema.json": encoded,
        "schema.js": "export default " + encoded.strip() + ";\n",
        "NativeProps.swift": "\n".join(swift),
        "NativeProps.kt": "\n".join(kotlin),
        "components.py": "\n".join(python),
        "README.md": "\n".join(docs),
    }
    compact = json.dumps(to_jsonable(spec), separators=(",", ":"), sort_keys=True)
    templates = Path(__file__).with_name("templates")
    for extension in ("swift", "kt"):
        template = (templates / f"contracts.{extension}").read_text(encoding="utf-8")
        # Escaped Swift strings and Kotlin raw strings have different interpolation rules.
        data = (
            compact.replace("\\", "\\\\")
            if extension == "swift"
            else ",\n".join(
                '"""' + compact[i : i + 8000].replace("$", "${'$'}") + '"""' for i in range(0, len(compact), 8000)
            )
        )
        outputs[f"PNContracts.{extension}"] = template.replace("{{fingerprint}}", fingerprint()).replace(
            "{{specification}}", data
        )
    from .module_codegen import generate_modules

    outputs.update(generate_modules())
    paths = []
    for name, contents in outputs.items():
        path = destination / name
        path.write_text(contents, encoding="utf-8")
        paths.append(path)
    return paths

Web Proxy Viewer  |  New URL  |  Original Page