Generate wire and data model API shims
This commit is contained in:
@@ -28,6 +28,45 @@ TARGETS = {
|
||||
"LibreMetaverse.StructuredData": ROOT / "crates/libremetaverse-structured-data/src/generated.rs",
|
||||
}
|
||||
|
||||
MAIN_TARGET = ROOT / "crates/libremetaverse/src/generated.rs"
|
||||
WIRE_NAMESPACES = (
|
||||
"LibreMetaverse.Assets",
|
||||
"LibreMetaverse.Formatters",
|
||||
"LibreMetaverse.Imaging",
|
||||
"LibreMetaverse.ImportExport",
|
||||
"LibreMetaverse.Materials",
|
||||
"LibreMetaverse.Messages",
|
||||
"LibreMetaverse.Packets",
|
||||
"LibreMetaverse.Rendering",
|
||||
)
|
||||
WIRE_ROOT_TYPES = {
|
||||
"T:LibreMetaverse.Animation",
|
||||
"T:LibreMetaverse.BitPack",
|
||||
"T:LibreMetaverse.Bumpiness",
|
||||
"T:LibreMetaverse.FieldType",
|
||||
"T:LibreMetaverse.GLTFMaterialUpdate",
|
||||
"T:LibreMetaverse.Grass",
|
||||
"T:LibreMetaverse.GrassDefinition",
|
||||
"T:LibreMetaverse.Interfaces.IMessage",
|
||||
"T:LibreMetaverse.LiveParticle",
|
||||
"T:LibreMetaverse.MappingType",
|
||||
"T:LibreMetaverse.MediaControls",
|
||||
"T:LibreMetaverse.MediaEntry",
|
||||
"T:LibreMetaverse.MediaPermission",
|
||||
"T:LibreMetaverse.NameValue",
|
||||
"T:LibreMetaverse.NameValue.ClassType",
|
||||
"T:LibreMetaverse.NameValue.SendtoType",
|
||||
"T:LibreMetaverse.NameValue.ValueType",
|
||||
"T:LibreMetaverse.PacketFrequency",
|
||||
"T:LibreMetaverse.PermissionMask",
|
||||
"T:LibreMetaverse.PermissionWho",
|
||||
"T:LibreMetaverse.Permissions",
|
||||
"T:LibreMetaverse.Shininess",
|
||||
"T:LibreMetaverse.TextureAttributes",
|
||||
"T:LibreMetaverse.Tree",
|
||||
"T:LibreMetaverse.TreeDefinition",
|
||||
}
|
||||
|
||||
UNDERLYING = {
|
||||
"System.SByte": "i8", "System.Byte": "u8", "System.Int16": "i16", "System.UInt16": "u16",
|
||||
"System.Int32": "i32", "System.UInt32": "u32", "System.Int64": "i64", "System.UInt64": "u64",
|
||||
@@ -134,6 +173,15 @@ def render_body(signature: str, member_id: str, error_model: str, trait: bool) -
|
||||
return f" {signature} {{ {failure} }}"
|
||||
|
||||
|
||||
def rust_constant_value(item: dict) -> str:
|
||||
value = item["value"]["value"]
|
||||
if item["type"] == "System.String":
|
||||
return json.dumps(value)
|
||||
if item["type"] == "System.Boolean":
|
||||
return value.lower()
|
||||
return value
|
||||
|
||||
|
||||
def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str]], mapper: mapping.Mapper) -> str:
|
||||
rust_name = type_name(type_row)
|
||||
if item["kind"] == "enum":
|
||||
@@ -179,8 +227,7 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
|
||||
row = member_rows[member["doc_id"]]
|
||||
lines.append(f" /// C# member: `{member['doc_id']}`.")
|
||||
if member["kind"] == "constant":
|
||||
value = member["value"]["value"]
|
||||
lines.append(f" {row['rust_signature']} = {value};")
|
||||
lines.append(f" {row['rust_signature']} = {rust_constant_value(member)};")
|
||||
continue
|
||||
signatures = row["rust_signature"].split(" ; ")
|
||||
for index, signature in enumerate(signatures):
|
||||
@@ -199,7 +246,53 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[int, int]]]:
|
||||
def render_pending_type(item: dict, type_row: dict) -> str:
|
||||
rust_name = type_name(type_row)
|
||||
names = generic_names(item)
|
||||
suffix = generic_suffix(names)
|
||||
lines = [f"/// Pending catalog type `{item['doc_id']}`; completed by a later milestone issue."]
|
||||
if item["kind"] == "interface":
|
||||
lines.append(f"pub trait {rust_name}{suffix} {{}}")
|
||||
elif names:
|
||||
tuple_type = "(" + ", ".join(names) + ("," if len(names) == 1 else "") + ")"
|
||||
lines += [f"pub struct {rust_name}{suffix} {{", f" _marker: std::marker::PhantomData<{tuple_type}>,", "}"]
|
||||
else:
|
||||
lines.append(f"pub struct {rust_name};")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def wire_type(item: dict) -> bool:
|
||||
return (
|
||||
item["doc_id"] in WIRE_ROOT_TYPES
|
||||
or item["doc_id"].startswith("T:LibreMetaverse.Primitive")
|
||||
or item["namespace"].startswith(WIRE_NAMESPACES)
|
||||
)
|
||||
|
||||
|
||||
def render_modules(items: list[tuple[dict, dict, bool]], member_rows: dict[str, dict[str, str]], mapper: mapping.Mapper) -> str:
|
||||
tree: dict = {"items": [], "children": {}}
|
||||
for item, type_row, complete in items:
|
||||
node = tree
|
||||
for segment in filter(None, type_row["rust_module"].split("::")):
|
||||
node = node["children"].setdefault(segment, {"items": [], "children": {}})
|
||||
node["items"].append((item, type_row, complete))
|
||||
|
||||
def render_node(node: dict) -> list[str]:
|
||||
lines = []
|
||||
for item, type_row, complete in node["items"]:
|
||||
rendered = render_type(item, type_row, member_rows, mapper) if complete else render_pending_type(item, type_row)
|
||||
lines += [rendered, ""]
|
||||
for name, child in sorted(node["children"].items()):
|
||||
body = render_node(child)
|
||||
lines.append(f"pub mod {name} {{")
|
||||
lines += [(" " + line) if line else "" for line in body]
|
||||
lines += ["}", ""]
|
||||
return lines
|
||||
|
||||
return "\n".join(render_node(tree))
|
||||
|
||||
|
||||
def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[int, int, bool]]]:
|
||||
type_rows, resolved = mapping.build_type_rows(catalog)
|
||||
type_by_id = {row["csharp_type_id"]: row for row in type_rows}
|
||||
interfaces = {
|
||||
@@ -216,7 +309,7 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
|
||||
coverage = {}
|
||||
for assembly in catalog["assemblies"]:
|
||||
name = assembly["identity"]["name"]
|
||||
if name not in TARGETS:
|
||||
if name not in TARGETS and name != "LibreMetaverse":
|
||||
continue
|
||||
chunks = [
|
||||
"// @generated by tools/generate_api_shims.py; do not edit by hand.",
|
||||
@@ -227,11 +320,16 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
|
||||
"#![allow(non_snake_case)]",
|
||||
"",
|
||||
]
|
||||
for item in assembly["types"]:
|
||||
chunks += [render_type(item, type_by_id[item["doc_id"]], member_rows, mapper), ""]
|
||||
selected = assembly["types"] if name in TARGETS else [item for item in assembly["types"] if wire_type(item)]
|
||||
selected_ids = {item["doc_id"] for item in selected}
|
||||
rendered_items = [
|
||||
(item, type_by_id[item["doc_id"]], item["doc_id"] in selected_ids)
|
||||
for item in assembly["types"]
|
||||
]
|
||||
chunks.append(render_modules(rendered_items, member_rows, mapper))
|
||||
source = format_rust("\n".join(chunks).rstrip() + "\n")
|
||||
expected_types = {item["doc_id"] for item in assembly["types"]}
|
||||
expected_members = {member["doc_id"] for item in assembly["types"] for member in item["members"]}
|
||||
expected_types = selected_ids
|
||||
expected_members = {member["doc_id"] for item in selected for member in item["members"]}
|
||||
emitted_types = set(re.findall(r"C# type: `(.*)`\.", source))
|
||||
emitted_members = set(re.findall(r"C# member: `(.*)`\.", source))
|
||||
if emitted_types != expected_types or emitted_members != expected_members:
|
||||
@@ -240,12 +338,12 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
|
||||
f"types missing/stale={len(expected_types - emitted_types)}/{len(emitted_types - expected_types)}, "
|
||||
f"members missing/stale={len(expected_members - emitted_members)}/{len(emitted_members - expected_members)}"
|
||||
)
|
||||
outputs[TARGETS[name]] = source
|
||||
coverage[name] = (len(assembly["types"]), sum(len(item["members"]) for item in assembly["types"]))
|
||||
outputs[TARGETS.get(name, MAIN_TARGET)] = source
|
||||
coverage[name] = (len(selected), sum(len(item["members"]) for item in selected), len(selected) == len(assembly["types"]))
|
||||
return outputs, coverage
|
||||
|
||||
|
||||
def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int]]) -> str:
|
||||
def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) -> str:
|
||||
lines = [
|
||||
"# Callable shim coverage", "", "Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.", "",
|
||||
"| Assembly | Types | Members | Status |", "|---|---:|---:|---|",
|
||||
@@ -256,15 +354,22 @@ def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int]]) -> str:
|
||||
types = len(assembly["types"])
|
||||
members = sum(len(item["members"]) for item in assembly["types"])
|
||||
if name in coverage:
|
||||
got_types, got_members = coverage[name]
|
||||
if (types, members) != (got_types, got_members):
|
||||
got_types, got_members, complete = coverage[name]
|
||||
if complete and (types, members) != (got_types, got_members):
|
||||
raise ValueError(f"incomplete generated slice for {name}")
|
||||
status = "callable failure-only shim"
|
||||
total_types += types
|
||||
total_members += members
|
||||
status = "callable failure-only shim" if complete else f"partial callable shim ({got_types:,} types / {got_members:,} members)"
|
||||
total_types += got_types
|
||||
total_members += got_members
|
||||
else:
|
||||
status = "pending milestone issue"
|
||||
lines.append(f"| `{name}` | {types:,} | {members:,} | {status} |")
|
||||
if "LibreMetaverse" in coverage and not coverage["LibreMetaverse"][2]:
|
||||
lines += [
|
||||
"",
|
||||
"The partial `LibreMetaverse` row is milestone issue #7's wire/data slice: packets, messages, assets,",
|
||||
"GLTF, archives/import-export, materials, rendering DTOs, bit packing, permissions, primitives, and",
|
||||
"texture/media/particle records. Other main-assembly types remain pending shells for issues #8-#10.",
|
||||
]
|
||||
lines += ["", f"Current callable coverage: **{total_types:,} types / {total_members:,} members**.", ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user