#!/usr/bin/env python3
"""The byte-size ladder: JSON -> MessagePack -> Protobuf -> Avro, DDIA 2e Ch. 5.

The same small record encoded five ways. Text JSON spells out every field name
and value. MessagePack is a binary JSON, but with no schema it must still carry
every field name. Protocol Buffers replaces the names with 1-byte field tags
(the schema holds the names). Avro drops even the tags, storing bare values in
schema order. Each rung down the ladder sheds a layer of self-description.

Run it:
    uv run --with msgpack --with fastavro --with grpcio-tools --with protobuf \
        python3 encoding_size_ladder.py
(Protobuf's .proto is compiled at runtime via grpc_tools, so no separate protoc
step is needed. Deterministic -> the sizes reproduce exactly.)
"""
import io
import json
import os
import sys
import tempfile

# The record from the book's example (Figure in "Formats for Encoding Data").
RECORD = {"userName": "Martin", "favoriteNumber": 1337,
          "interests": ["daydreaming", "hacking"]}


def protobuf_bytes():
    work = tempfile.mkdtemp()
    proto = os.path.join(work, "person.proto")
    with open(proto, "w") as f:
        f.write('syntax = "proto3";\n'
                'message Person {\n'
                '  string user_name = 1;\n'
                '  int64  favorite_number = 2;\n'
                '  repeated string interests = 3;\n'
                '}\n')
    from grpc_tools import protoc
    protoc.main(["protoc", f"-I{work}", f"--python_out={work}", proto])
    sys.path.insert(0, work)
    import person_pb2
    p = person_pb2.Person(user_name=RECORD["userName"],
                          favorite_number=RECORD["favoriteNumber"],
                          interests=RECORD["interests"])
    return p.SerializeToString()


def avro_bytes():
    import fastavro
    schema = {"type": "record", "name": "Person", "fields": [
        {"name": "userName", "type": "string"},
        {"name": "favoriteNumber", "type": ["null", "long"], "default": None},
        {"name": "interests", "type": {"type": "array", "items": "string"}}]}
    buf = io.BytesIO()
    fastavro.schemaless_writer(buf, fastavro.parse_schema(schema), RECORD)
    return buf.getvalue()


def main():
    import msgpack

    j = json.dumps(RECORD, separators=(",", ":")).encode()
    m = msgpack.packb(RECORD)
    pb = protobuf_bytes()
    av = avro_bytes()

    print(f"record: {json.dumps(RECORD)}\n")
    print("encoded size, and what each format spends its bytes on:")
    rows = [
        ("JSON", j, "text: every field name + value + punctuation"),
        ("MessagePack", m, "binary, but schemaless -> still carries every field name"),
        ("Protocol Buffers", pb, "field names become 1-byte tags (the schema holds the names)"),
        ("Avro", av, "bare values in schema order -- no names, no tags"),
    ]
    for name, b, note in rows:
        print(f"    {name:17s} {len(b):3d} bytes   {note}")

    # How much of JSON is just field names?
    names = "userName" + "favoriteNumber" + "interests"
    print(f"\nof JSON's {len(j)} bytes, {len(names)} are field-name characters "
          f"({100 * len(names) // len(j)}%) -- repeated in every record.")
    print(f"the ladder: {len(j)} -> {len(m)} -> {len(pb)} -> {len(av)} bytes, "
          f"each rung dropping a layer of self-description.")


if __name__ == "__main__":
    main()
