Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / infra / serializer.py: 100%
76 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Serialization adapters for JSON and YAML formats."""
6from __future__ import annotations
8import importlib.util as _importlib_util
9import json
10from types import ModuleType
11from typing import Any, Final, cast
13from bijux_cli.core.enums import OutputFormat
15_orjson_spec = _importlib_util.find_spec("orjson")
16_yaml_spec = _importlib_util.find_spec("yaml")
18_orjson_mod: ModuleType | None
19try:
20 import orjson as _orjson_mod
21except ImportError:
22 _orjson_mod = None
23_ORJSON: Final[ModuleType | None] = _orjson_mod
25_yaml_mod: ModuleType | None
26try:
27 import yaml as _yaml_mod
28except ImportError:
29 _yaml_mod = None
30_YAML: Final[ModuleType | None] = _yaml_mod
33class SerializationError(RuntimeError):
34 """Raised when serialization or deserialization fails."""
37def _yaml_dump(obj: Any, pretty: bool) -> str:
38 """Serialize an object to YAML."""
39 if _YAML is None:
40 raise SerializationError("PyYAML is required for YAML operations")
41 dumped = _YAML.safe_dump(
42 obj,
43 sort_keys=False,
44 default_flow_style=not pretty,
45 indent=2 if pretty else None,
46 )
47 return dumped or ""
50class OrjsonSerializer:
51 """Serializer that handles JSON (and YAML via PyYAML)."""
53 def __init__(self, telemetry: Any | None) -> None:
54 """Initialize with telemetry."""
55 self._telemetry = telemetry
57 def dumps(self, obj: Any, *, fmt: OutputFormat, pretty: bool) -> str:
58 """Serialize an object to JSON or YAML."""
59 if fmt is OutputFormat.JSON:
60 try:
61 if _ORJSON is not None:
62 option = _ORJSON.OPT_INDENT_2 if pretty else 0
63 return cast(
64 str,
65 _ORJSON.dumps(obj, option=option).decode("utf-8"),
66 )
67 return json.dumps(obj, indent=2 if pretty else None)
68 except Exception as exc:
69 raise SerializationError(f"Failed to serialize json: {exc}") from exc
70 if fmt is OutputFormat.YAML:
71 return _yaml_dump(obj, pretty)
72 raise SerializationError(f"Unsupported format: {fmt}")
74 def dumps_bytes(self, obj: Any, *, fmt: OutputFormat, pretty: bool) -> bytes:
75 """Serialize an object to bytes."""
76 return self.dumps(obj, fmt=fmt, pretty=pretty).encode("utf-8")
78 def loads(
79 self,
80 data: str | bytes,
81 *,
82 fmt: OutputFormat,
83 pretty: bool,
84 ) -> Any:
85 """Deserialize JSON or YAML data."""
86 if fmt is OutputFormat.JSON:
87 try:
88 return json.loads(data)
89 except Exception as exc:
90 raise SerializationError(f"Failed to deserialize json: {exc}") from exc
91 if fmt is OutputFormat.YAML:
92 if _YAML is None:
93 raise SerializationError("PyYAML is required for YAML operations")
94 return _YAML.safe_load(data)
95 raise SerializationError(f"Unsupported format: {fmt}")
98class PyYAMLSerializer:
99 """Serializer restricted to YAML format."""
101 def __init__(self, telemetry: Any | None) -> None:
102 """Initialize with telemetry."""
103 if _YAML is None:
104 raise SerializationError("PyYAML is not installed")
105 self._telemetry = telemetry
107 def dumps(self, obj: Any, *, fmt: OutputFormat, pretty: bool) -> str:
108 """Serialize an object to YAML."""
109 if fmt is not OutputFormat.YAML:
110 raise SerializationError("PyYAMLSerializer only supports YAML")
111 return _yaml_dump(obj, pretty)
113 def dumps_bytes(self, obj: Any, *, fmt: OutputFormat, pretty: bool) -> bytes:
114 """Serialize an object to bytes."""
115 return self.dumps(obj, fmt=fmt, pretty=pretty).encode("utf-8")
117 def loads(
118 self,
119 data: str | bytes,
120 *,
121 fmt: OutputFormat,
122 pretty: bool,
123 ) -> Any:
124 """Deserialize YAML data."""
125 if fmt is not OutputFormat.YAML:
126 raise SerializationError("PyYAMLSerializer only supports YAML")
127 return _YAML.safe_load(data) if _YAML is not None else None
130def serializer_for(
131 fmt: OutputFormat, telemetry: Any | None
132) -> OrjsonSerializer | PyYAMLSerializer:
133 """Return the best serializer for the requested format."""
134 if fmt is OutputFormat.JSON:
135 return OrjsonSerializer(telemetry)
136 if fmt is OutputFormat.YAML:
137 return PyYAMLSerializer(telemetry)
138 raise SerializationError(f"Unsupported format: {fmt}")
141__all__ = [
142 "SerializationError",
143 "OrjsonSerializer",
144 "PyYAMLSerializer",
145 "serializer_for",
146]