Source code for bot_api.internal.json_util
import json
import inspect
from typing import Type, Any
from robocode_tank_royale import schema
[docs]
def to_camel_case(snake_str: str) -> str:
"""Converts a snake_case string to camelCase."""
components = snake_str.split("_")
return components[0] + "".join(x.title() for x in components[1:])
[docs]
def to_snake_case(camel_str: str) -> str:
"""Converts a camelCase string to snake_case."""
s = "".join(["_" + c.lower() if c.isupper() else c for c in camel_str])
return s.lstrip("_")
def _sanitize_type_str(type_str: str) -> str:
"""
Sanitizes a type string to get a clean class name.
e.g. "robocode_tank_royale.schema.bullet_state.BulletState | None" -> "BulletState"
"""
if type_str.startswith("list["):
# Handle list types like "list[BulletState]"
type_str = type_str[5:-1] # Remove "list[" and "]"
# Remove the module path and any type annotations like | None
return type_str.split(".")[-1].split(" ")[0]
[docs]
class MessageEncoder(json.JSONEncoder):
[docs]
def default(self, o: Any) -> Any:
if isinstance(o, schema.Color):
return o.value
# Handle bot-api Color objects (from graphics.color module)
if hasattr(o, "to_hex_color") and callable(o.to_hex_color):
return o.to_hex_color()
if hasattr(o, "__dict__"):
return {to_camel_case(k): v for k, v in o.__dict__.items() if not k.startswith("_")}
return super().default(o)
[docs]
def from_json(json_str_or_dict: str | dict[str, Any]) -> schema.Message:
"""
Deserializes a JSON string into a Message object.
"""
if isinstance(json_str_or_dict, str):
json_str = json_str_or_dict
obj = json.loads(json_str)
else:
obj = json_str_or_dict
msg_type = obj.get("type")
if not msg_type:
raise ValueError(
"JSON object does not have a 'type' field for message deserialization"
)
if msg_type in schema.CLASS_MAP:
event_class = schema.CLASS_MAP[msg_type]
return _from_json_object(obj, event_class)
# Fallback for other message types if needed
# For now, we only handle events
raise ValueError(f"Unknown message type: {msg_type}")
def _from_json_object(obj: dict[str, Any], klass: Type[Any]) -> Any:
"""
Recursively deserializes a dictionary into an object of the specified class.
"""
if not inspect.isclass(klass):
return obj
kwargs = {}
sig = inspect.signature(klass.__init__)
for key, value in obj.items():
key = to_snake_case(key)
if key not in sig.parameters:
# `key` not needed to construct the class.
continue
if isinstance(value, dict):
param = sig.parameters[key]
param_type = schema.CLASS_MAP.get(_sanitize_type_str(str(param.annotation)))
if param_type is not None:
kwargs[key] = _from_json_object(value, param_type) # type: ignore
else:
kwargs[key] = value
elif isinstance(value, list):
param = sig.parameters[key]
param_type = schema.CLASS_MAP.get(_sanitize_type_str(str(param.annotation)), None)
if param_type is not None:
deserialized_items = []
for item in value: # type: ignore
assert isinstance(item, dict)
item_type = schema.CLASS_MAP[item["type"]] if 'type' in item else param_type
deserialized_items.append(_from_json_object(item, item_type)) # type: ignore
kwargs[key] = deserialized_items
else:
kwargs[key] = value
elif key.lower().endswith("color"):
annotation = _sanitize_type_str(str(sig.parameters[key].annotation))
if annotation == "Color":
if value is not None and isinstance(value, str):
kwargs[key] = schema.Color(value=value)
else:
kwargs[key] = None
else:
# Not actually a Color type, just a field ending with "color"
kwargs[key] = value
else:
kwargs[key] = value
result = klass(**kwargs)
return result
[docs]
def to_json(obj: schema.Message) -> str:
"""
Serializes a Message object into a JSON string.
"""
return json.dumps(obj, cls=MessageEncoder, sort_keys=True, indent=4)