Skip to content

Commit 9c4978c

Browse files
committed
add auto-generated json schema for crossref_xml writer
1 parent e11d0b0 commit 9c4978c

5 files changed

Lines changed: 308 additions & 733 deletions

File tree

commonmeta/cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,5 +309,40 @@ def version() -> None:
309309
click.echo(f"commonmeta-py {version}")
310310

311311

312+
@cli.command("jsonschema")
313+
@click.option(
314+
"--schema",
315+
"schema_name",
316+
type=click.Choice(["crossref_xml"], case_sensitive=False),
317+
default="crossref_xml",
318+
show_default=True,
319+
)
320+
@click.option(
321+
"--output",
322+
"output_path",
323+
"-o",
324+
type=click.Path(dir_okay=False, path_type=str),
325+
)
326+
def jsonschema(schema_name: str, output_path: str | None) -> None:
327+
"""Generate JSON Schema for a Marshmallow schema.
328+
329+
Prints to stdout by default, or writes to `--output`.
330+
"""
331+
332+
import os
333+
334+
from commonmeta.jsonschema_generator import generate_jsonschema
335+
336+
schema = generate_jsonschema(schema_name.lower())
337+
payload = json.dumps(schema, option=json.OPT_INDENT_2 | json.OPT_SORT_KEYS)
338+
339+
if output_path:
340+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
341+
with open(output_path, "wb") as f:
342+
f.write(payload)
343+
else:
344+
click.echo(payload.decode("utf-8"))
345+
346+
312347
if __name__ == "__main__":
313348
cli()

commonmeta/jsonschema_generator.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Generate JSON Schema documents for Marshmallow schemas.
2+
3+
We keep this isolated from the core library logic: Commonmeta already ships
4+
hand-authored JSON Schemas under `commonmeta/resources/` for validation.
5+
6+
Note: `marshmallow-jsonschema` (0.13.0) imports `pkg_resources` at import time.
7+
`pkg_resources` (from setuptools) is deprecated and may be absent in modern
8+
environments. To avoid adding setuptools just for this, we install a minimal
9+
runtime shim for `pkg_resources.get_distribution` before importing the library.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import importlib.metadata
15+
import sys
16+
import types
17+
from typing import Any
18+
19+
from marshmallow import Schema
20+
21+
22+
def _install_pkg_resources_shim() -> None:
23+
"""Install a minimal `pkg_resources` shim into `sys.modules`.
24+
25+
`marshmallow-jsonschema` only uses `pkg_resources.get_distribution(...).version`
26+
to populate `__version__`.
27+
"""
28+
29+
if "pkg_resources" in sys.modules:
30+
return
31+
32+
shim = types.ModuleType("pkg_resources")
33+
34+
def get_distribution(dist_name: str):
35+
try:
36+
version = importlib.metadata.version(dist_name)
37+
except importlib.metadata.PackageNotFoundError:
38+
version = "0.0.0"
39+
return types.SimpleNamespace(version=version)
40+
41+
shim.get_distribution = get_distribution # type: ignore[attr-defined]
42+
sys.modules["pkg_resources"] = shim
43+
44+
45+
def marshmallow_to_jsonschema(schema: Schema) -> dict[str, Any]:
46+
"""Convert a Marshmallow `Schema` instance to a JSON Schema dict."""
47+
48+
_install_pkg_resources_shim()
49+
50+
from marshmallow_jsonschema import JSONSchema
51+
52+
base: dict[str, Any] = JSONSchema().dump(schema)
53+
# marshmallow-jsonschema doesn't add the $schema keyword; add it for clarity.
54+
return {"$schema": "http://json-schema.org/draft-07/schema#", **base}
55+
56+
57+
def generate_jsonschema(name: str) -> dict[str, Any]:
58+
"""Generate a JSON Schema by symbolic name.
59+
60+
Currently supported:
61+
- `crossref_xml`: the pre-serialization Crossref writer dict.
62+
"""
63+
64+
if name == "crossref_xml":
65+
from .writers.crossref_xml_writer import CrossrefXMLSchema
66+
67+
schema = marshmallow_to_jsonschema(CrossrefXMLSchema())
68+
# Match existing resource naming and intent.
69+
schema["$id"] = "crossref-v5.4.0.json"
70+
schema["title"] = "Crossref XML Writer v5.4.0"
71+
schema["description"] = (
72+
"JSON Schema for validating output before converting to Crossref XML."
73+
)
74+
return schema
75+
76+
raise ValueError(f"Unknown schema: {name}")

commonmeta/metadata.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,12 @@ def write(self, to: str = "commonmeta", **kwargs) -> bytes | None:
313313
self.email = kwargs.get("email", None)
314314
self.registrant = kwargs.get("registrant", None)
315315
output = write_crossref_xml(self)
316+
317+
# Validate the intermediate dict against JSON schema before converting to XML.
318+
# self.write_errors = json_schema_errors(output, schema=to)
319+
# if self.write_errors is not None:
320+
# raise CrossrefError(self.write_errors)
321+
316322
head = {
317323
"depositor": self.depositor,
318324
"email": self.email,

0 commit comments

Comments
 (0)