ofplang-validate 0.1.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.
@@ -0,0 +1,19 @@
1
+ """ofplang.validate -- validator for Object-flow Programming Language v0."""
2
+
3
+ from ofplang.validate.validator import (
4
+ EXTENSION_TOLERANT,
5
+ MODES,
6
+ STRICT,
7
+ Diagnostic,
8
+ ValidationResult,
9
+ validate,
10
+ )
11
+
12
+ __all__ = [
13
+ "Diagnostic",
14
+ "ValidationResult",
15
+ "validate",
16
+ "STRICT",
17
+ "EXTENSION_TOLERANT",
18
+ "MODES",
19
+ ]
@@ -0,0 +1,9 @@
1
+ """Enable `python -m ofplang <file>...`.
2
+
3
+ Intent: mirror the console-script entry point so the CLI is reachable without an
4
+ installed script, which is convenient in dev checkouts and CI.
5
+ """
6
+
7
+ from ofplang.validate.cli import main
8
+
9
+ raise SystemExit(main())
@@ -0,0 +1,178 @@
1
+ """Command-line interface for the ofplang v0 validator.
2
+
3
+ Intent: this is a thin presentation layer over :func:`ofplang.validate.validate` — it
4
+ does no validation itself, it only parses arguments, drives the library over one
5
+ or more files, and renders results. Keeping all logic in the library means the
6
+ CLI cannot drift from the conformance-tested behavior.
7
+
8
+ Usage:
9
+ ofp-validate [--mode {strict,extension-tolerant}] [--format {text,json}]
10
+ [-q/--quiet] [--no-color] <file>...
11
+
12
+ Also invocable as `python -m ofplang.validate`, and (once installed) as the
13
+ `validate` subcommand of the umbrella `ofp` CLI.
14
+
15
+ Exit codes (linter convention):
16
+ 0 every file is valid
17
+ 1 at least one file has validation errors
18
+ 2 usage / input error (bad arguments, missing input file)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ from ofplang.validate import validate
29
+ from ofplang.validate.validator import EXTENSION_TOLERANT, STRICT, ValidationResult
30
+
31
+ # Exit codes are part of the CLI contract (scripts/CI depend on them).
32
+ EXIT_OK = 0
33
+ EXIT_INVALID = 1
34
+ EXIT_USAGE = 2
35
+
36
+ # ANSI colors, applied only when writing to a TTY and not disabled.
37
+ _RED = "\033[31m"
38
+ _GREEN = "\033[32m"
39
+ _DIM = "\033[2m"
40
+ _RESET = "\033[0m"
41
+
42
+
43
+ def _build_parser() -> argparse.ArgumentParser:
44
+ parser = argparse.ArgumentParser(
45
+ prog="ofp-validate",
46
+ description="Validate ofplang v0 documents.",
47
+ )
48
+ parser.add_argument("paths", nargs="+", metavar="FILE", help="document(s) to validate")
49
+ parser.add_argument(
50
+ "--mode",
51
+ choices=[STRICT, EXTENSION_TOLERANT],
52
+ default=STRICT,
53
+ help="validation mode (default: strict)",
54
+ )
55
+ parser.add_argument(
56
+ "--format",
57
+ choices=["text", "json"],
58
+ default="text",
59
+ help="output format (default: text)",
60
+ )
61
+ parser.add_argument(
62
+ "-q",
63
+ "--quiet",
64
+ action="store_true",
65
+ help="suppress per-diagnostic lines; show only the summary",
66
+ )
67
+ parser.add_argument("--no-color", action="store_true", help="disable ANSI color output")
68
+ return parser
69
+
70
+
71
+ def _color_enabled(no_color: bool) -> bool:
72
+ # Color only when explicitly allowed AND attached to a terminal, so piped or
73
+ # redirected output stays clean and parseable.
74
+ return not no_color and sys.stdout.isatty()
75
+
76
+
77
+ def _render_text(results: list[tuple[str, ValidationResult]], quiet: bool, color: bool) -> str:
78
+ """Human-oriented report. One block per file, then a one-line summary."""
79
+ def c(text: str, code: str) -> str:
80
+ return f"{code}{text}{_RESET}" if color else text
81
+
82
+ lines: list[str] = []
83
+ total_errors = 0
84
+ invalid_files = 0
85
+ multi = len(results) > 1
86
+
87
+ for path, result in results:
88
+ if result.ok:
89
+ # Only announce OK files explicitly when validating several, so
90
+ # single-file runs stay terse.
91
+ if not quiet and multi:
92
+ lines.append(f"{path}: {c('OK', _GREEN)}")
93
+ continue
94
+ invalid_files += 1
95
+ total_errors += len(result.diagnostics)
96
+ if multi:
97
+ lines.append(f"{path}:")
98
+ if not quiet:
99
+ for d in result.diagnostics:
100
+ # Prefer a concrete source position as the locator; fall back to
101
+ # the logical path. When both exist, show the path as a dim
102
+ # trailing detail (position primary, path for context).
103
+ if d.location:
104
+ locator = d.location
105
+ detail = f" {c(d.path, _DIM)}" if d.path else ""
106
+ else:
107
+ locator = d.path or "<root>"
108
+ detail = ""
109
+ msg = f" {d.message}" if d.message else ""
110
+ indent = " " if multi else ""
111
+ lines.append(f"{indent}{locator}: {c('error', _RED)} {d.code}{detail}{msg}")
112
+
113
+ if total_errors == 0:
114
+ lines.append(
115
+ c(f"all valid ({len(results)} file{'s' if len(results) != 1 else ''})", _GREEN)
116
+ )
117
+ else:
118
+ lines.append(
119
+ c(f"{total_errors} error{'s' if total_errors != 1 else ''} in "
120
+ f"{invalid_files} of {len(results)} file{'s' if len(results) != 1 else ''}", _RED)
121
+ )
122
+ return "\n".join(lines)
123
+
124
+
125
+ def _render_json(results: list[tuple[str, ValidationResult]]) -> str:
126
+ """Machine-readable report for CI/editor integration."""
127
+ payload = {
128
+ "ok": all(r.ok for _, r in results),
129
+ "results": [
130
+ {
131
+ "file": path,
132
+ "ok": result.ok,
133
+ "diagnostics": [
134
+ {
135
+ "code": d.code,
136
+ "path": d.path,
137
+ "message": d.message,
138
+ "file": d.file,
139
+ "line": d.line,
140
+ "col": d.col,
141
+ }
142
+ for d in result.diagnostics
143
+ ],
144
+ }
145
+ for path, result in results
146
+ ],
147
+ }
148
+ return json.dumps(payload, indent=2, ensure_ascii=False)
149
+
150
+
151
+ def main(argv: list[str] | None = None) -> int:
152
+ parser = _build_parser()
153
+ args = parser.parse_args(argv)
154
+
155
+ # Pre-check inputs so a mistyped/missing path is a usage error (exit 2)
156
+ # rather than being reported as an in-document diagnostic.
157
+ missing = [p for p in args.paths if not Path(p).is_file()]
158
+ if missing:
159
+ for p in missing:
160
+ print(f"ofp-validate: cannot open {p!r}: no such file", file=sys.stderr)
161
+ return EXIT_USAGE
162
+
163
+ # Validate each file via the library (the sole source of truth).
164
+ results: list[tuple[str, ValidationResult]] = [
165
+ (p, validate(p, mode=args.mode)) for p in args.paths
166
+ ]
167
+
168
+ if args.format == "json":
169
+ print(_render_json(results))
170
+ else:
171
+ print(_render_text(results, args.quiet, _color_enabled(args.no_color)))
172
+
173
+ # Exit code reflects the worst outcome across all inputs.
174
+ return EXIT_OK if all(r.ok for _, r in results) else EXIT_INVALID
175
+
176
+
177
+ if __name__ == "__main__": # pragma: no cover - module entry
178
+ raise SystemExit(main())