c4studio 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c4studio/__init__.py +28 -0
- c4studio/__main__.py +5 -0
- c4studio/cli/__init__.py +0 -0
- c4studio/cli/main.py +329 -0
- c4studio/diagnostics.py +84 -0
- c4studio/generators/__init__.py +4 -0
- c4studio/generators/flowchart.py +354 -0
- c4studio/generators/json_export.py +678 -0
- c4studio/generators/mermaid.py +169 -0
- c4studio/generators/mermaid_common.py +65 -0
- c4studio/graph/__init__.py +37 -0
- c4studio/graph/view_graph.py +1176 -0
- c4studio/icons.py +100 -0
- c4studio/models/__init__.py +135 -0
- c4studio/models/deployment.py +96 -0
- c4studio/models/documentation.py +63 -0
- c4studio/models/elements.py +141 -0
- c4studio/models/enums.py +152 -0
- c4studio/models/views.py +336 -0
- c4studio/models/workspace.py +226 -0
- c4studio/parser/__init__.py +4 -0
- c4studio/parser/docs.py +109 -0
- c4studio/parser/dsl.py +2254 -0
- c4studio/parser/expressions.py +310 -0
- c4studio/parser/implied.py +79 -0
- c4studio/parser/json_parser.py +726 -0
- c4studio/parser/locations.py +156 -0
- c4studio/parser/sourcemap.py +52 -0
- c4studio/py.typed +0 -0
- c4studio/render.py +146 -0
- c4studio/renderer/diagram-render.mjs +9757 -0
- c4studio/themes.py +146 -0
- c4studio/webapp/__init__.py +12 -0
- c4studio/webapp/graph.py +132 -0
- c4studio/webapp/loader.py +64 -0
- c4studio/webapp/model_graph.py +227 -0
- c4studio/webapp/server.py +713 -0
- c4studio/webapp/static/assets/index-CibNm4nc.js +68 -0
- c4studio/webapp/static/assets/index-Px2v-U7I.css +1 -0
- c4studio/webapp/static/index.html +13 -0
- c4studio-0.2.0.dist-info/METADATA +283 -0
- c4studio-0.2.0.dist-info/RECORD +45 -0
- c4studio-0.2.0.dist-info/WHEEL +4 -0
- c4studio-0.2.0.dist-info/entry_points.txt +3 -0
- c4studio-0.2.0.dist-info/licenses/LICENSE +21 -0
c4studio/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""c4studio – parse Structurizr DSL/JSON and generate C4 Mermaid diagrams."""
|
|
2
|
+
|
|
3
|
+
from c4studio.models import (
|
|
4
|
+
Component,
|
|
5
|
+
Container,
|
|
6
|
+
Person,
|
|
7
|
+
Relationship,
|
|
8
|
+
SoftwareSystem,
|
|
9
|
+
View,
|
|
10
|
+
ViewType,
|
|
11
|
+
Workspace,
|
|
12
|
+
)
|
|
13
|
+
from c4studio.parser import parse_dsl, parse_json
|
|
14
|
+
from c4studio.generators import MermaidGenerator
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"parse_dsl",
|
|
18
|
+
"parse_json",
|
|
19
|
+
"MermaidGenerator",
|
|
20
|
+
"Workspace",
|
|
21
|
+
"SoftwareSystem",
|
|
22
|
+
"Container",
|
|
23
|
+
"Component",
|
|
24
|
+
"Person",
|
|
25
|
+
"Relationship",
|
|
26
|
+
"View",
|
|
27
|
+
"ViewType",
|
|
28
|
+
]
|
c4studio/__main__.py
ADDED
c4studio/cli/__init__.py
ADDED
|
File without changes
|
c4studio/cli/main.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""c4studio CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from c4studio.diagnostics import Diagnostic, Severity
|
|
10
|
+
from c4studio.generators.flowchart import FlowchartGenerator
|
|
11
|
+
from c4studio.generators.mermaid import MermaidGenerator
|
|
12
|
+
from c4studio.models import Workspace
|
|
13
|
+
from c4studio.render import RenderError, render_view
|
|
14
|
+
from c4studio.webapp.graph import is_supported
|
|
15
|
+
from c4studio.webapp.loader import WorkspaceLoadError, load_workspace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_workspace(path: Path) -> Workspace:
|
|
19
|
+
"""Load a workspace, mapping loader errors to a CLI-friendly message."""
|
|
20
|
+
try:
|
|
21
|
+
workspace = load_workspace(path)
|
|
22
|
+
except WorkspaceLoadError as exc:
|
|
23
|
+
raise click.BadParameter(str(exc)) from exc
|
|
24
|
+
for warning in workspace.parse_warnings:
|
|
25
|
+
click.echo(f"warning: {warning}", err=True)
|
|
26
|
+
return workspace
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
@click.version_option()
|
|
31
|
+
def cli() -> None:
|
|
32
|
+
"""c4studio – parse Structurizr files and generate C4 diagrams."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@cli.command("generate")
|
|
36
|
+
@click.argument("input_file", type=click.Path(exists=True, path_type=Path))
|
|
37
|
+
@click.option(
|
|
38
|
+
"--output",
|
|
39
|
+
"-o",
|
|
40
|
+
type=click.Path(path_type=Path),
|
|
41
|
+
default=None,
|
|
42
|
+
help="Output directory (default: print to stdout).",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--view",
|
|
46
|
+
"-v",
|
|
47
|
+
"view_key",
|
|
48
|
+
default=None,
|
|
49
|
+
help="Only generate for this view key.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--format",
|
|
53
|
+
"-f",
|
|
54
|
+
"fmt",
|
|
55
|
+
type=click.Choice(["mermaid", "flowchart"], case_sensitive=False),
|
|
56
|
+
default="mermaid",
|
|
57
|
+
show_default=True,
|
|
58
|
+
help=(
|
|
59
|
+
"mermaid: Mermaid C4 syntax (C4Context/C4Container/C4Component). "
|
|
60
|
+
"flowchart: Mermaid flowchart + subgraph — renders more reliably on "
|
|
61
|
+
"GitHub and dense models, and covers dynamic, deployment and "
|
|
62
|
+
"filtered views too."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
def generate(
|
|
66
|
+
input_file: Path, output: Path | None, view_key: str | None, fmt: str
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Generate diagrams from INPUT_FILE (DSL or JSON)."""
|
|
69
|
+
workspace = _load_workspace(input_file)
|
|
70
|
+
if fmt.lower() == "flowchart":
|
|
71
|
+
diagrams = FlowchartGenerator(workspace).generate_all()
|
|
72
|
+
else:
|
|
73
|
+
diagrams = MermaidGenerator(workspace).generate_all()
|
|
74
|
+
if view_key:
|
|
75
|
+
if view_key not in diagrams:
|
|
76
|
+
available = ", ".join(diagrams) or "(none)"
|
|
77
|
+
raise click.ClickException(
|
|
78
|
+
f"View '{view_key}' not found. Available: {available}"
|
|
79
|
+
)
|
|
80
|
+
diagrams = {view_key: diagrams[view_key]}
|
|
81
|
+
|
|
82
|
+
if output is None:
|
|
83
|
+
for key, content in diagrams.items():
|
|
84
|
+
if len(diagrams) > 1:
|
|
85
|
+
click.echo(f"--- {key} ---")
|
|
86
|
+
click.echo(content)
|
|
87
|
+
if len(diagrams) > 1:
|
|
88
|
+
click.echo()
|
|
89
|
+
else:
|
|
90
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
ext = ".mmd"
|
|
92
|
+
for key, content in diagrams.items():
|
|
93
|
+
out_path = output / f"{key}{ext}"
|
|
94
|
+
out_path.write_text(content, encoding="utf-8")
|
|
95
|
+
click.echo(f"Written: {out_path}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command("render")
|
|
99
|
+
@click.argument("input_file", type=click.Path(exists=True, path_type=Path))
|
|
100
|
+
@click.option(
|
|
101
|
+
"--output",
|
|
102
|
+
"-o",
|
|
103
|
+
type=click.Path(path_type=Path),
|
|
104
|
+
default=None,
|
|
105
|
+
help="Output directory (default: print to stdout).",
|
|
106
|
+
)
|
|
107
|
+
@click.option(
|
|
108
|
+
"--view",
|
|
109
|
+
"-v",
|
|
110
|
+
"view_key",
|
|
111
|
+
default=None,
|
|
112
|
+
help="Only render this view key.",
|
|
113
|
+
)
|
|
114
|
+
@click.option(
|
|
115
|
+
"--padding",
|
|
116
|
+
type=int,
|
|
117
|
+
default=24,
|
|
118
|
+
show_default=True,
|
|
119
|
+
help="Blank margin around the diagram, in pixels.",
|
|
120
|
+
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"--no-title",
|
|
123
|
+
is_flag=True,
|
|
124
|
+
default=False,
|
|
125
|
+
help="Omit the heading above the diagram (the SVG <title> stays).",
|
|
126
|
+
)
|
|
127
|
+
@click.option(
|
|
128
|
+
"--no-legend",
|
|
129
|
+
is_flag=True,
|
|
130
|
+
default=False,
|
|
131
|
+
help="Omit the legend of element styles used by the view.",
|
|
132
|
+
)
|
|
133
|
+
def render(
|
|
134
|
+
input_file: Path,
|
|
135
|
+
output: Path | None,
|
|
136
|
+
view_key: str | None,
|
|
137
|
+
padding: int,
|
|
138
|
+
no_title: bool,
|
|
139
|
+
no_legend: bool,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Render diagrams from INPUT_FILE as standalone SVG.
|
|
142
|
+
|
|
143
|
+
No browser and no server: the diagrams are laid out and painted by the
|
|
144
|
+
same code the web app runs, bundled for Node. This is the only command
|
|
145
|
+
that needs Node.js installed; set C4STUDIO_NODE if it is not on
|
|
146
|
+
PATH. Views the renderer cannot draw (custom, image) are skipped.
|
|
147
|
+
"""
|
|
148
|
+
workspace = _load_workspace(input_file)
|
|
149
|
+
views = [v for v in workspace.views if is_supported(v)]
|
|
150
|
+
if view_key:
|
|
151
|
+
views = [v for v in views if v.key == view_key]
|
|
152
|
+
if not views:
|
|
153
|
+
available = ", ".join(v.key for v in workspace.views if is_supported(v))
|
|
154
|
+
raise click.ClickException(
|
|
155
|
+
f"View '{view_key}' not found or not renderable. "
|
|
156
|
+
f"Available: {available or '(none)'}"
|
|
157
|
+
)
|
|
158
|
+
if not views:
|
|
159
|
+
raise click.ClickException("this workspace has no renderable views")
|
|
160
|
+
|
|
161
|
+
if output is not None:
|
|
162
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
try:
|
|
164
|
+
for view in views:
|
|
165
|
+
svg = render_view(
|
|
166
|
+
workspace,
|
|
167
|
+
view,
|
|
168
|
+
padding=padding,
|
|
169
|
+
show_title=not no_title,
|
|
170
|
+
show_legend=not no_legend,
|
|
171
|
+
)
|
|
172
|
+
if output is None:
|
|
173
|
+
click.echo(svg, nl=False)
|
|
174
|
+
else:
|
|
175
|
+
out_path = output / f"{view.key}.svg"
|
|
176
|
+
out_path.write_text(svg, encoding="utf-8")
|
|
177
|
+
click.echo(f"Written: {out_path}")
|
|
178
|
+
except RenderError as exc:
|
|
179
|
+
raise click.ClickException(str(exc)) from exc
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@cli.command("export")
|
|
183
|
+
@click.argument("input_file", type=click.Path(exists=True, path_type=Path))
|
|
184
|
+
@click.option(
|
|
185
|
+
"--output",
|
|
186
|
+
"-o",
|
|
187
|
+
type=click.Path(path_type=Path),
|
|
188
|
+
default=None,
|
|
189
|
+
help="Output file (default: print to stdout).",
|
|
190
|
+
)
|
|
191
|
+
def export(input_file: Path, output: Path | None) -> None:
|
|
192
|
+
"""Export INPUT_FILE (DSL or JSON) as Structurizr workspace JSON.
|
|
193
|
+
|
|
194
|
+
The output round-trips with structurizr.com, Structurizr Lite, and
|
|
195
|
+
this package's own JSON parser.
|
|
196
|
+
"""
|
|
197
|
+
from c4studio.generators.json_export import export_json, export_json_file
|
|
198
|
+
|
|
199
|
+
workspace = _load_workspace(input_file)
|
|
200
|
+
if output is None:
|
|
201
|
+
click.echo(export_json(workspace), nl=False)
|
|
202
|
+
else:
|
|
203
|
+
if output.parent != Path(""):
|
|
204
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
export_json_file(workspace, output)
|
|
206
|
+
click.echo(f"Written: {output}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@cli.command("check")
|
|
210
|
+
@click.argument("input_file", type=click.Path(allow_dash=True, path_type=Path))
|
|
211
|
+
@click.option(
|
|
212
|
+
"--path",
|
|
213
|
+
"source_path",
|
|
214
|
+
type=click.Path(path_type=Path),
|
|
215
|
+
help=(
|
|
216
|
+
"File the source on stdin belongs to. Diagnostics are reported "
|
|
217
|
+
"against it, and relative !include/!docs/!adrs targets resolve "
|
|
218
|
+
"next to it."
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
@click.option(
|
|
222
|
+
"--json",
|
|
223
|
+
"as_json",
|
|
224
|
+
is_flag=True,
|
|
225
|
+
help="Emit diagnostics as JSON, for editors and other tools.",
|
|
226
|
+
)
|
|
227
|
+
@click.option(
|
|
228
|
+
"--strict",
|
|
229
|
+
is_flag=True,
|
|
230
|
+
help="Exit non-zero on warnings as well as errors.",
|
|
231
|
+
)
|
|
232
|
+
def check(
|
|
233
|
+
input_file: Path, source_path: Path | None, as_json: bool, strict: bool
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Report problems in INPUT_FILE without generating anything.
|
|
236
|
+
|
|
237
|
+
INPUT_FILE may be ``-`` to read DSL from stdin, which is how an editor
|
|
238
|
+
checks a buffer that has not been saved. Pass ``--path`` with it so
|
|
239
|
+
diagnostics name the real file and relative directives resolve.
|
|
240
|
+
|
|
241
|
+
Exits 1 when the file has errors (or, with --strict, warnings), so it
|
|
242
|
+
can gate a CI job. Warnings cover DSL the parser understood but did not
|
|
243
|
+
implement — those constructs are skipped, and this is where that
|
|
244
|
+
becomes visible.
|
|
245
|
+
"""
|
|
246
|
+
import json as json_module
|
|
247
|
+
|
|
248
|
+
from c4studio.parser.dsl import ParseError, parse_dsl, parse_dsl_file
|
|
249
|
+
|
|
250
|
+
diagnostics: list[Diagnostic] = []
|
|
251
|
+
from_stdin = str(input_file) == "-"
|
|
252
|
+
try:
|
|
253
|
+
if from_stdin:
|
|
254
|
+
resolved = source_path.resolve() if source_path is not None else None
|
|
255
|
+
workspace = parse_dsl(
|
|
256
|
+
click.get_text_stream("stdin").read(),
|
|
257
|
+
base_dir=resolved.parent if resolved is not None else None,
|
|
258
|
+
path=resolved,
|
|
259
|
+
)
|
|
260
|
+
elif input_file.suffix.lower() == ".json":
|
|
261
|
+
workspace = _load_workspace(input_file)
|
|
262
|
+
else:
|
|
263
|
+
workspace = parse_dsl_file(input_file)
|
|
264
|
+
diagnostics = list(workspace.diagnostics)
|
|
265
|
+
except ParseError as error:
|
|
266
|
+
# Every problem found, not just the one that stopped parsing.
|
|
267
|
+
diagnostics = list(error.diagnostics)
|
|
268
|
+
|
|
269
|
+
if as_json:
|
|
270
|
+
click.echo(json_module.dumps([d.to_dict() for d in diagnostics], indent=2))
|
|
271
|
+
else:
|
|
272
|
+
for diagnostic in diagnostics:
|
|
273
|
+
click.echo(f"{diagnostic.severity.value}: {diagnostic}", err=True)
|
|
274
|
+
if not diagnostics:
|
|
275
|
+
label = source_path if from_stdin and source_path else input_file
|
|
276
|
+
click.echo(f"{label}: no problems found")
|
|
277
|
+
|
|
278
|
+
errors = [d for d in diagnostics if d.severity is Severity.ERROR]
|
|
279
|
+
if errors or (strict and diagnostics):
|
|
280
|
+
raise SystemExit(1)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@cli.command("list-views")
|
|
284
|
+
@click.argument("input_file", type=click.Path(exists=True, path_type=Path))
|
|
285
|
+
def list_views(input_file: Path) -> None:
|
|
286
|
+
"""List all views defined in INPUT_FILE."""
|
|
287
|
+
workspace = _load_workspace(input_file)
|
|
288
|
+
if not workspace.views:
|
|
289
|
+
click.echo("No views found.")
|
|
290
|
+
return
|
|
291
|
+
click.echo(f"{'Key':<30} {'Type':<20} {'Element ID'}")
|
|
292
|
+
click.echo("-" * 65)
|
|
293
|
+
for view in workspace.views:
|
|
294
|
+
click.echo(f"{view.key:<30} {view.type:<20} {view.element_id}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@cli.command("webapp")
|
|
298
|
+
@click.argument("path", type=click.Path(exists=True, path_type=Path))
|
|
299
|
+
@click.option("--port", default=8090, show_default=True, help="Port to listen on.")
|
|
300
|
+
@click.option("--host", default="127.0.0.1", show_default=True)
|
|
301
|
+
@click.option(
|
|
302
|
+
"--no-browser",
|
|
303
|
+
is_flag=True,
|
|
304
|
+
default=False,
|
|
305
|
+
help="Do not open a browser window automatically.",
|
|
306
|
+
)
|
|
307
|
+
def webapp(path: Path, port: int, host: str, no_browser: bool) -> None:
|
|
308
|
+
"""Launch the React web application backend for PATH.
|
|
309
|
+
|
|
310
|
+
PATH may be a directory (browsed as the source root) or a single source
|
|
311
|
+
file (loaded eagerly, with its parent directory as the root).
|
|
312
|
+
"""
|
|
313
|
+
from c4studio.webapp.server import run_server
|
|
314
|
+
|
|
315
|
+
if path.is_dir():
|
|
316
|
+
root: Path = path
|
|
317
|
+
initial: Path | None = None
|
|
318
|
+
else:
|
|
319
|
+
root = path.parent
|
|
320
|
+
initial = path
|
|
321
|
+
|
|
322
|
+
if not no_browser:
|
|
323
|
+
import threading
|
|
324
|
+
import webbrowser
|
|
325
|
+
|
|
326
|
+
url = f"http://{host}:{port}"
|
|
327
|
+
threading.Timer(1.0, webbrowser.open, args=(url,)).start()
|
|
328
|
+
|
|
329
|
+
run_server(root=root, initial=initial, host=host, port=port)
|
c4studio/diagnostics.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Structured parse problems, for editors and CI.
|
|
2
|
+
|
|
3
|
+
A diagnostic is a machine-readable parse problem: where it happened, how
|
|
4
|
+
bad it is, and what to say about it. The parser produces these instead of
|
|
5
|
+
formatting positions into message strings, so an editor can place a marker
|
|
6
|
+
and a CI job can decide whether to fail.
|
|
7
|
+
|
|
8
|
+
Positions are 1-based lines, matching how editors and compilers report
|
|
9
|
+
them. Columns are optional: the tokeniser tracks lines only, so a
|
|
10
|
+
diagnostic without a column marks the whole line.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Severity(str, Enum):
|
|
22
|
+
"""How much a problem matters.
|
|
23
|
+
|
|
24
|
+
``ERROR`` means the source could not be understood as written;
|
|
25
|
+
``WARNING`` means it parsed but something was skipped or ignored, which
|
|
26
|
+
is the usual outcome for DSL features this parser does not implement.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
ERROR = "error"
|
|
30
|
+
WARNING = "warning"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Diagnostic:
|
|
35
|
+
"""One parse problem.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
message: Human-readable description, without position information.
|
|
39
|
+
severity: :class:`Severity` of the problem.
|
|
40
|
+
path: File the problem is in. ``None`` when parsing a bare string,
|
|
41
|
+
and resolved through the include source map otherwise — a
|
|
42
|
+
problem inside an ``!include``-ed fragment names that fragment,
|
|
43
|
+
not the file that included it.
|
|
44
|
+
line: 1-based line within ``path``.
|
|
45
|
+
column: 1-based start column, when known.
|
|
46
|
+
end_column: Exclusive 1-based end column, when known.
|
|
47
|
+
code: Short stable identifier for the kind of problem, so editors
|
|
48
|
+
and CI can filter without matching on message text.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
message: str
|
|
52
|
+
severity: Severity = Severity.ERROR
|
|
53
|
+
path: Path | None = None
|
|
54
|
+
line: int | None = None
|
|
55
|
+
column: int | None = None
|
|
56
|
+
end_column: int | None = None
|
|
57
|
+
code: str = ""
|
|
58
|
+
|
|
59
|
+
def __str__(self) -> str:
|
|
60
|
+
"""Render as ``path:line: message``, omitting unknown parts.
|
|
61
|
+
|
|
62
|
+
This is the form the CLI prints and the form retained in the legacy
|
|
63
|
+
``Workspace.parse_warnings`` list of strings.
|
|
64
|
+
"""
|
|
65
|
+
where = ""
|
|
66
|
+
if self.path is not None:
|
|
67
|
+
where = f"{self.path}:"
|
|
68
|
+
if self.line is not None:
|
|
69
|
+
where += f"{self.line}:"
|
|
70
|
+
elif self.line is not None:
|
|
71
|
+
where = f"Line {self.line}:"
|
|
72
|
+
return f"{where} {self.message}".strip()
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
75
|
+
"""Serialise for ``c4studio check --json``."""
|
|
76
|
+
return {
|
|
77
|
+
"path": str(self.path) if self.path is not None else None,
|
|
78
|
+
"line": self.line,
|
|
79
|
+
"column": self.column,
|
|
80
|
+
"endColumn": self.end_column,
|
|
81
|
+
"severity": self.severity.value,
|
|
82
|
+
"code": self.code,
|
|
83
|
+
"message": self.message,
|
|
84
|
+
}
|