Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / services / diagnostics / docs.py: 100%
47 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"""Provides the concrete implementation of the API specification writing service.
6This module defines the `Docs` class, which implements the `DocsProtocol`.
7It is responsible for serializing API specification data into formats like
8JSON or YAML and writing the resulting documents to the filesystem. It
9integrates with observability and telemetry services to log its activities.
10"""
12from __future__ import annotations
14from collections.abc import Mapping
15import os
16from pathlib import Path
17from typing import Any
19from injector import inject
21from bijux_cli.core.enums import OutputFormat
22from bijux_cli.infra.contracts import Serializer
23from bijux_cli.services.contracts import ObservabilityProtocol, TelemetryProtocol
24from bijux_cli.services.diagnostics.contracts import DocsProtocol
25from bijux_cli.services.errors import ServiceError
28class Docs(DocsProtocol):
29 """A service for writing API specification documents to disk.
31 This class implements the `DocsProtocol` to handle the serialization and
32 writing of specifications (e.g., OpenAPI, JSON Schema) to files.
34 Attributes:
35 _observability (ObservabilityProtocol): The logging service.
36 _serializer (Serializer): The serializer adapter for output.
37 _telemetry (TelemetryProtocol): The telemetry service for event tracking.
38 _root (Path): The root directory where documents will be written.
39 """
41 @inject
42 def __init__(
43 self,
44 observability: ObservabilityProtocol,
45 serializer: Serializer,
46 telemetry: TelemetryProtocol,
47 root: str | Path | None = None,
48 ) -> None:
49 """Initializes the `Docs` service.
51 Args:
52 observability (ObservabilityProtocol): The service for logging.
53 serializer (Serializer): The serializer adapter for output.
54 telemetry (TelemetryProtocol): The service for event tracking.
55 root (str | Path | None): The root directory for writing documents.
56 It defaults to the `BIJUXCLI_DOCS_DIR` environment variable,
57 or "docs" if not set.
58 """
59 self._observability = observability
60 self._serializer = serializer
61 self._telemetry = telemetry
62 env_root = os.getenv("BIJUXCLI_DOCS_DIR")
63 root_dir = env_root if env_root else (root or "docs")
64 self._root = Path(root_dir)
65 self._root.mkdir(exist_ok=True, parents=True)
67 def render(
68 self, spec: Mapping[str, Any], *, fmt: OutputFormat, pretty: bool = False
69 ) -> str:
70 """Renders a specification dictionary to a string in the given format.
72 Args:
73 spec (dict[str, Any]): The specification dictionary to serialize.
74 fmt (OutputFormat): The desired output format (e.g., JSON, YAML).
76 Returns:
77 str: The serialized specification as a string.
79 Raises:
80 TypeError: If the underlying serializer returns a non-string result.
81 """
82 result = self._serializer.dumps(spec, fmt=fmt, pretty=pretty)
83 if not isinstance(result, str):
84 raise TypeError(
85 f"Expected str from serializer.dumps, got {type(result).__name__}"
86 )
87 return result
89 def write(
90 self,
91 spec: Mapping[str, Any],
92 *,
93 fmt: OutputFormat = OutputFormat.JSON,
94 name: str = "spec",
95 pretty: bool = False,
96 ) -> str:
97 """Writes a specification to a file and returns the path as a string.
99 This is a convenience wrapper around `write_sync`.
101 Args:
102 spec (dict[str, Any]): The specification dictionary to write.
103 fmt (OutputFormat): The output format. Defaults to `OutputFormat.JSON`.
104 name (str): The base name for the output file. Defaults to 'spec'.
106 Returns:
107 str: The absolute path to the written file.
108 """
109 path = self.write_sync(spec, fmt, name, pretty)
110 return str(path)
112 def write_sync(
113 self,
114 spec: Mapping[str, Any],
115 fmt: OutputFormat,
116 name: str | Path,
117 pretty: bool = False,
118 ) -> Path:
119 """Writes the specification to a file synchronously.
121 This method handles path resolution, serializes the `spec` dictionary,
122 and writes the content to the final destination file.
124 Args:
125 spec (dict[str, Any]): The specification dictionary to write.
126 fmt (OutputFormat): The desired output format.
127 name (str | Path): The path or base name for the output file.
129 Returns:
130 Path: The `Path` object pointing to the written file.
132 Raises:
133 ServiceError: If writing to the file fails due to an `OSError`.
134 """
135 final_path = None
136 try:
137 final_path = Path(name).expanduser().resolve()
138 if final_path.is_dir():
139 final_path = final_path / f"spec.{fmt.value}"
140 final_path.parent.mkdir(parents=True, exist_ok=True)
141 content = self.render(spec, fmt=fmt, pretty=pretty)
142 final_path.write_text(content, encoding="utf-8")
143 self._observability.log("info", f"Wrote docs to {final_path}", extra={})
144 self._telemetry.event(
145 "docs_written", {"path": str(final_path), "format": fmt.value}
146 )
147 return final_path
148 except OSError as exc:
149 self._telemetry.event(
150 "docs_write_failed",
151 {
152 "path": (
153 str(final_path) if final_path is not None else "<unresolved>"
154 ),
155 "error": str(exc),
156 },
157 )
158 raise ServiceError(f"Unable to write spec: {exc}", http_status=403) from exc
160 def close(self) -> None:
161 """Closes the service. This is a no-op for this implementation."""
162 return
165__all__ = ["Docs"]