Source code for oasapi.prune

import copy
import itertools
from collections import defaultdict
from typing import Dict, Tuple, List

from oasapi.common import (
    get_elements,
    REFERENCE_SECTIONS,
    JSPATH_PATHS_REFERENCES,
    JSPATH_REFERENCES,
    JSPATH_COMPONENTS,
    JSPATH_TAGS,
    JSPATH_OPERATION_TAGS,
    JSPATH_SECURITY,
    JSPATH_ENDPOINTS,
)
from oasapi.events import (
    ReferenceNotUsedFilterAction,
    SecurityDefinitionNotUsedFilterAction,
    OAuth2ScopeNotUsedFilterAction,
    TagNotUsedFilterAction,
    FilterAction,
    PathsEmptyFilterError,
)


def prune_unused_global_items(swagger):
    """Prune the swagger (in place) of its unused global items
    in the definitions, responses and parameters global sections"""

    def decompose_reference(references):
        return set(
            tuple(reference[2:].split("/"))
            for _, reference, _ in references
            if reference.startswith("#/")
        )

    # start by taking all references use in /paths
    refs = refs_new = decompose_reference(get_elements(swagger, JSPATH_PATHS_REFERENCES))

    while True:
        swagger_new = {section: {} for section in REFERENCE_SECTIONS}
        for rt, obj in refs_new:
            # handle only local references
            swagger_new[rt][obj] = swagger[rt][obj]

        refs_new = decompose_reference(get_elements(swagger_new, JSPATH_REFERENCES))

        if refs_new.issubset(refs):
            break

        refs |= refs_new

    actions = []
    for _, _, ref_path in get_elements(swagger, JSPATH_COMPONENTS):
        if ref_path not in refs:
            # the reference is not used, remove it
            rt, obj = ref_path
            del swagger[rt][obj]
            actions.append(
                ReferenceNotUsedFilterAction(path=(rt, obj), reason="reference not used")
            )

    # remove sections that are left empty
    for section in REFERENCE_SECTIONS:
        if section in swagger and not swagger[section]:
            del swagger[section]

    return swagger, actions


def prune_unused_security_definitions(swagger):
    """Prune the swagger (in place) of its unused securityDefinitions or oauth scopes"""
    if "securityDefinitions" not in swagger:
        return swagger, []

    security_jspath = JSPATH_SECURITY

    # detect security definitions used and for which scope
    secdefs_used = defaultdict(set)
    for sec_name, sec_scopes, _ in get_elements(swagger, security_jspath):
        secdefs_used[sec_name].update(sec_scopes)

    # iterate existing securityDefinitions to check if they are used and if their scopes are used
    actions = []
    for sec_name, sec_def in swagger["securityDefinitions"].copy().items():
        if sec_name not in secdefs_used:
            del swagger["securityDefinitions"][sec_name]
            actions.append(
                SecurityDefinitionNotUsedFilterAction(
                    path=("securityDefinitions", sec_name), reason="security definition not used"
                )
            )

        elif "scopes" in sec_def:
            for scope_name, scope_def in sec_def["scopes"].copy().items():
                if scope_name not in secdefs_used[sec_name]:
                    del swagger["securityDefinitions"][sec_name]["scopes"][scope_name]
                    actions.append(
                        OAuth2ScopeNotUsedFilterAction(
                            path=("securityDefinitions", sec_name, "scopes", scope_name),
                            reason="oauth2 scope not used",
                        )
                    )

    # remove securityDefinitions if empty
    if not swagger["securityDefinitions"]:
        del swagger["securityDefinitions"]

    return swagger, actions


def prune_unused_tags(swagger):
    """Prune the swagger (in place) of its unused tags"""
    if "tags" not in swagger:
        return swagger, []

    tags_jspath = JSPATH_OPERATION_TAGS

    # detect security definitions used and for which scope
    tags_used = set().union(*[tags_list for _, tags_list, _ in get_elements(swagger, tags_jspath)])

    # iterate existing securityDefinitions to check if they are used and if their scopes are used
    actions = []
    for _, tag_name, (*path_before_name, path_name) in get_elements(swagger, JSPATH_TAGS):
        if tag_name not in tags_used:
            actions.append(
                TagNotUsedFilterAction(
                    path=tuple(path_before_name), reason=f"tag definition for '{tag_name}' not used"
                )
            )

    swagger["tags"] = [tag for tag in swagger["tags"] if tag["name"] in tags_used]

    # remove tags if empty
    if not swagger["tags"]:
        del swagger["tags"]

    return swagger, actions


def prune_empty_paths(swagger):
    """Prune the swagger (in place) of its empty paths (ie paths with no verb)"""

    # list all operations (paths without any operation are not included
    actions = []
    for endpoint_name, endpoint, path in get_elements(swagger, JSPATH_ENDPOINTS):
        if not endpoint or len(endpoint) == 1 and "parameters" in endpoint:
            # endpoint is empty, remove it
            del swagger["paths"][endpoint_name]

            actions.append(
                PathsEmptyFilterError(
                    path=path, reason=f"path '{endpoint_name}' has no operations defined"
                )
            )

    return swagger, actions


[docs]def prune(swagger: Dict) -> Tuple[Dict, List[FilterAction]]: """ Prune a swagger specification. The pruning removed from the swagger the following elements: - unused global definitions/responses/parameters - unused securityDefinition/scopes - unused tags - empty paths (i.e. endpoints with no verbs) :param swagger: the swagger spec :return: pruned swagger, a set of actions """ swagger = copy.deepcopy(swagger) actions = list( itertools.chain( *[ prune_operation(swagger)[1] for prune_operation in [ prune_empty_paths, prune_unused_tags, prune_unused_global_items, prune_unused_security_definitions, ] ] ) ) return swagger, actions