Skip to content

utils

Utility functions for codemeta.json.

validate_codemeta

validate_codemeta(codemeta: dict) -> list

Validate the codemeta.json file against the codemeta.jsonld schema.

Parameters:

Name Type Description Default
codemeta dict

codemeta.json file as a dictionary.

required

Returns:

Name Type Description
invalid_fields list

List of invalid fields.

Source code in src/somesy/codemeta/utils.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def validate_codemeta(codemeta: dict) -> list:
    """Validate the codemeta.json file against the codemeta.jsonld schema.

    Args:
        codemeta (dict): codemeta.json file as a dictionary.

    Returns:
        invalid_fields (list): List of invalid fields.

    """
    schema_path = Path(__file__).parent / "schema-3.jsonld"
    invalid_fields = []

    # Check for required fields
    required_fields = {"@context", "@type", "name"}
    missing_fields = [field for field in required_fields if field not in codemeta]
    if missing_fields:
        invalid_fields.extend(missing_fields)

    # Validate @context
    codemeta_context = codemeta.get("@context", [])
    if isinstance(codemeta_context, str):
        codemeta_context = [codemeta_context]
    if V3_CONTEXT not in codemeta_context:
        invalid_fields.append("@context")
        logger.warning(
            "The @context field in codemeta.json does not contain the Codemeta v3.1 context."
        )

    try:
        # Load Codemeta JSON-LD Schema
        with open(schema_path, "r", encoding="utf-8") as f:
            schema = json.load(f)

        # Use schema's context to avoid network fetch issues
        schema_context = schema["@context"]
        codemeta_copy = copy.deepcopy(codemeta)
        codemeta_copy["@context"] = schema_context

        # Expand and compact to validate terms
        expanded = jsonld.expand(codemeta_copy)
        compacted = jsonld.compact(expanded, schema_context)
        if not isinstance(compacted, dict):
            raise TypeError("Codemeta compaction did not produce an object")

        # Check for unmapped prefixes while allowing prefixes declared by CodeMeta.
        compacted_keys = set(compacted)
        for key in compacted_keys:
            if ":" in key and key.split(":", 1)[0] not in schema_context:
                logger.error(f"Invalid schema reference found: {key}")
                invalid_fields.append(key)

        # Check for unsupported terms by comparing original and compacted keys.
        # Only top-level keys are checked on purpose: nested objects may use any
        # valid schema.org term, which the CodeMeta context does not list, so
        # recursing would report false positives.
        original_keys = set(codemeta.keys())
        compacted_keys = set(compacted)

        # Remove @type from comparison as it might be handled differently in compaction
        original_keys.discard("@type")
        if "@type" in compacted_keys:
            compacted_keys.remove("@type")

        unsupported_terms = original_keys - compacted_keys
        if unsupported_terms:
            logger.warning(f"Unsupported terms found: {sorted(unsupported_terms)}")
            invalid_fields.extend(unsupported_terms)

    except Exception as e:  # noqa: BLE001 - validation must report all JSON-LD failures
        logger.error(f"Codemeta validation failed: {e}")
        return ["Validation error"]

    return list(set(invalid_fields))  # Remove duplicates