Source code for rs_metadata.profile.runner

"""Running every profile check over one record.

Kept out of ``__init__`` so the package's namespace is only re-exports, and so
the ordering and early-exit policy have an obvious home.
"""

from __future__ import annotations

from ..concepts import ParsedSource
from ..report import Diagnostic
from .context import ProfileContext
from .formats import check_formats
from .identifiers import check_agents, check_identifiers
from .jsonld import check_context, check_feature_list, check_unknown_properties
from .licenses import check_licenses
from .placeholders import check_placeholders
from .recommendations import check_recommendations
from .schema_checks import check_schema
from .terminology import check_edam, check_vocabularies
from .types import check_types

__all__ = ["CHECKS", "validate"]


#: Every check, in the order their findings read best.
#:
#: Order matters for the reader, not for correctness: structural problems come
#: before value-level ones, because a record with no ``@context`` has bigger
#: problems than a non-canonical license URL. Diagnostics are re-sorted by
#: severity before rendering, so this is a tiebreak rather than a guarantee.
CHECKS = (
    check_context,
    check_feature_list,
    check_schema,
    check_types,
    check_formats,
    check_unknown_properties,
    check_licenses,
    check_identifiers,
    check_agents,
    check_edam,
    check_vocabularies,
    check_placeholders,
    check_recommendations,
)


[docs] def validate(parsed: ParsedSource) -> list[Diagnostic]: """Validate an anchor record against the LUMC profile. Parameters ---------- parsed : ParsedSource The parsed ``codemeta.json``, carrying its source map so findings can report line numbers. Returns ------- list of Diagnostic Every finding, in check order. The caller sorts by severity before rendering. See Also -------- rs_metadata.core.validate : The full pipeline, including consistency. Examples -------- >>> from rs_metadata.concepts import ParsedSource >>> record = { ... "@context": ["https://w3id.org/codemeta/3.1"], ... "@type": "SoftwareSourceCode", ... "name": "MyTool", ... "description": "A tool.", ... "version": "1.0.0", ... "identifier": "https://doi.org/10.5281/zenodo.1", ... "author": [{"@type": "Person", ... "@id": "https://orcid.org/0000-0002-1825-0097", ... "givenName": "Josiah", "familyName": "Carberry"}], ... "license": "https://spdx.org/licenses/Apache-2.0", ... "codeRepository": "https://github.com/lumc/mytool", ... "programmingLanguage": ["Python"], ... "applicationCategory": "Command-line tool", ... "schema:featureList": ["http://edamontology.org/operation_0292"], ... } >>> source = ParsedSource( ... adapter_id="codemeta", file="codemeta.json", format="CodeMeta", ... document=record, ... ) >>> findings = validate(source) A record satisfying the profile raises no errors: >>> [d for d in findings if d.severity.value == "error"] [] Recommendations are still reported, as one aggregated warning: >>> [d.code for d in findings if d.severity.value == "warning"] ['recommendation.missing'] Break one thing and it is named precisely: >>> del record["license"] >>> next(d.message for d in validate(source) ... if d.code == "profile.required-field") 'codemeta.json is missing the mandatory property "license" (license).' """ ctx = ProfileContext(parsed=parsed) context_findings = check_context(ctx) if any(finding.code == "profile.invalid-context" for finding in context_findings): # The record's vocabulary is not one this profile understands, so every # later check would be judging it against the wrong dictionary. Running # them anyway buries the one message that matters under a page of # complaints about properties that are perfectly valid where they came # from. Consistency checks against companion files still run: they do # not depend on the vocabulary being current. return context_findings return context_findings + [ diagnostic for check in CHECKS if check is not check_context for diagnostic in check(ctx) ]