Source code for fair_data_schema.cli

"""
CLI entry point for fair-data-schema.

Commands:
  validate  – validate a JSON instance against a schema (or a schema against the meta-schema)
  lint      – check all schema files for JSON syntax validity
  info      – show registered schema URIs
"""

from __future__ import annotations

from pathlib import Path
from typing import Annotated

import typer
from rich import print as rprint
from rich.console import Console
from rich.table import Table

from fair_data_schema import __version__
from fair_data_schema import registry as reg
from fair_data_schema import validator as val

app = typer.Typer(
    name="fair-data-schema",
    help="Tools for working with the FAIR Data JSON Schema dialect.",
    no_args_is_help=True,
)

console = Console()
err_console = Console(stderr=True, style="bold red")


# ── validate ─────────────────────────────────────────────────────────────────


[docs] @app.command() def validate( schema: Annotated[Path, typer.Argument(help="Path to the JSON Schema file.")], instance: Annotated[ Path | None, typer.Argument(help="Path to the JSON instance. If omitted, validates the schema itself."), ] = None, ) -> None: """Validate a JSON instance against a schema, or a schema against the meta-schema.""" if not schema.exists(): err_console.print(f"Schema file not found: {schema}") raise typer.Exit(code=1) if instance is not None and not instance.exists(): err_console.print(f"Instance file not found: {instance}") raise typer.Exit(code=1) errors = val.validate_file(schema, instance) if not errors: rprint(f"[green]✓[/green] Valid — {schema}" + (f" ← {instance}" if instance else "")) raise typer.Exit(code=0) suffix = f" ← {instance}" if instance else "" err_console.print(f"[red]✗[/red] {len(errors)} error(s) in {schema}{suffix}") for error in errors: err_console.print(f" • {error.message} (path: {list(error.absolute_path)})") raise typer.Exit(code=1)
# ── lint ──────────────────────────────────────────────────────────────────────
[docs] @app.command() def lint( directory: Annotated[ Path, typer.Argument(help="Directory to scan for JSON files."), ] = Path("."), ) -> None: """Check all JSON files in schemas/ and examples/ for syntax validity.""" search_root = directory.resolve() json_files = list(search_root.rglob("*.json")) if not json_files: rprint(f"[yellow]No JSON files found under {search_root}[/yellow]") raise typer.Exit(code=0) errors_found = False for path in sorted(json_files): if val.is_valid_json(path): rprint(f"[green]✓[/green] {path.relative_to(search_root)}") else: err_console.print(f"[red]✗[/red] Invalid JSON: {path.relative_to(search_root)}") errors_found = True raise typer.Exit(code=1 if errors_found else 0)
# ── info ──────────────────────────────────────────────────────────────────────
[docs] @app.command() def info() -> None: """Show registered schema URIs and their local file mappings.""" rprint(f"[bold]fair-data-schema[/bold] v{__version__}") rprint(f"Base URI: [cyan]{reg.BASE_URI}[/cyan]\n") table = Table("URI suffix", "Local path", title="Registered Schemas") for uri in reg.schema_uris(): suffix = uri.removeprefix(reg.BASE_URI) local_path = reg.resolve_uri(uri) exists_mark = "✓" if local_path.exists() else "✗ MISSING" table.add_row(suffix, f"{exists_mark} {local_path.name}") console.print(table)
# ── version ───────────────────────────────────────────────────────────────────
[docs] @app.command() def version() -> None: """Print the package version.""" rprint(f"fair-data-schema {__version__}")
if __name__ == "__main__": app()