question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Looking for a straight-forward (more convenient) way to programmatically delete named components of a path

See original GitHub issue

I have an OpenAPI definition where we are using multi-file layout for requestBodies, responses, and schemas; And also using openapi-cli to do some transformations via custom plugin/decorator;

I have a custom vendor extension i’m applying at path level, to determine whether to delete that path or not from the build. Example path object:

x-delete-path: true
post:
  operationId: blah
  summary: blah
  description: blah
  requestBody:
    $ref: ../components/requestBodies/blah.yaml
  responses:
    '200':
      $ref: ../components/responses/blah.yaml

The problem is that when writing the decorator, in the PathItem object, if I delete a path item, the related named requestBodies, responses, and schemas are not deleted, so they end up in the resulting build.

I discovered how to use the ctx.resolve() function to obtain the related requestBody from the $ref, but am not seeing a way to delete the named requestBody from that object that is returned.

It appears that openapi-cli is using the referenced filename to determine the name of the named object. Therefore, I suppose I could try to parse and store that $ref’d filename in the PathItem object’s enter or leave method, in order to delete that property in NamedRequestBodies, etc…

Any other thoughts or suggestions? Thanks for this awesome product.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:9 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
mjpieterscommented, Jun 30, 2021

For what it’s worth: we have solved this problem by using reference counting in a preprocessor:

  • a ref visitor increments counts for the #/components/.... paths in a map
  • a PathItem visitor with nested Operation visitor handles filtering of operations (and the leave hook on PathItem deletes paths if all operations are dropped from the output). As long as you clear all properties in the Operation object you can prevent traversal to any references, and so prevent their count going up.
  • on DefinitionRoot.leave, clear out components with no references. For schemas, we repeatedly process each item and decrement reference counts with a recursive utility function whenever a schema item is dropped, to catch transitive references.

Here is a rough outline of how we implemented this strategy; perhaps this can inform more specific support in openapi-core for this type of document processing. Any project specific parts are ‘templated’ by putting descriptive text in square brackets (e.g. [this is a placeholder for your own logic]):

/** @type { import('@redocly/openapi-core/src/config/config').PreprocessorsConfig } */
const preprocessors = {
  oas3: {
    '[rule name]': (options) => {
      // [configuration setup]
      // [bookkeeping setup used to determine when operations are to be dropped]

      // reference counts
      const tagsSeen = new Set()
      const componentsSeen = new Map()
      const operationsForPath = []

      // [utility functions used to process operations]

      // recurse over node (object / array) to decrease counts for any $ref
      // found. Used when removing component schemas to also clear out
      // transitive schema dependencies.
      function decreaseComponentCounts (node) {
        if (node?.$ref) {
          const count = componentsSeen.get(node.$ref)
          if (count > 1) componentsSeen.set(node.$ref, count - 1)
          else componentsSeen.delete(node.$ref)
        }
        if (Array.isArray(node)) {
          for (const elem of node) decreaseComponentCounts(elem)
        } else if (typeof node === 'object' && node !== null) {
          for (const elem of Object.values(node)) decreaseComponentCounts(elem)
        }
      }

      return {
        ref (node, { location }) {
          // Record what shared components are used across the document.
          // This visitor is not reached for operations we drop.
          if (node.$ref.startsWith('#/components/')) {
            componentsSeen.set(node.$ref, (componentsSeen.get(node.$ref) || 0) + 1)
          }
        },

        DefinitionRoot: {
          leave (root) {
            // remove any unreferenced schema, response or tags
            root.tags = root.tags.filter((tag) => tagsSeen.has(tag.name))

            for (const response of Object.keys(root.components?.responses || {})) {
              if (!componentsSeen.has(`#/components/responses/${response}`)) {
                delete root.components.responses[response]
              }
            }

            while (true) {
              const schemas = Object.keys(root.components?.schemas || {})
              const schemaCount = schemas.length
              for (const schema of schemas) {
                if (!componentsSeen.has(`#/components/schemas/${schema}`)) {
                  // traverse component to decrease reference counts to any
                  // components it references.
                  decreaseComponentCounts(root.components.schemas[schema])
                  delete root.components.schemas[schema]
                }
              }
              // repeat until no more schema components have been removed
              if (Object.keys(root.components?.schemas || {}).length === schemaCount) {
                break
              }
            }
          }
        },

        PathItem: {
          Operation (op, { parent, key, location }) {
            // Drop operations based on business rules
            if ([test criteria for dropping this operation]) {
              delete parent[key]
              // Clear all properties to prevent further traversal
              for (const prop of Object.keys(op)) {
                delete op[prop]
              }
            } else {
              // Operation is being kept; make note to keep the path item and
              // track what tags this parameter uses so we can clean up unused
              // tags.
              operationsForPath.push(key)
              for (const tag of op.tags || []) {
                tagsSeen.add(tag)
              }
            }
          },

          leave (pathItem, { parent, key, location }) {
            // Drop path items that have no operations remaining.
            if (operationsForPath.length === 0) {
              delete parent[key]
            }
            // reset per-path bookkeeping state
            operationsForPath.splice(0, operationsForPath.length)
          }
        }
      }
    }
  }
}

Depending on your OpenAPI document structure, you may have to expand the recursive component processing to headers responses, request bodies, etc too. I can imagine that in a more complex document, that the multi-pass approach for schemas would have to be extended to loop over the flattened Component collections. It just was not an issue for our project however.

1reaction
Kerry-at-VIPcommented, Jul 12, 2021

I found a much cleaner (for us) approach.

Instead of programmatically deleting paths, and then trying to clean up all the related named components, we are adding paths into the build using a plugin.

The benefit to this strategy, is that redoc doesn’t wire all the unnecessary named components in, because the linked path items are never included.

Prior to this change (for example), we were flagging a path for deletion with a flag like x-delete-path: true

Using the new strategy, the paths: object is completely empty by default. Then, based on criteria, we are adding paths in:

Paths:
  /Some/Path:
    $ref: paths/Some-Path.yaml

That way, the only named components that redoc pulls into the resulting build will be what is linked to in that path.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Delete directory with files in it? - php - Stack Overflow
I'm using rmdir(PATH . '/' . $value); to delete a folder, however, if there are files inside of it, I simply can't delete...
Read more >
Working With Files in Python
In this tutorial, you'll learn how you can work with files in Python by using built-in modules to perform practical tasks that involve...
Read more >
Remove folders from search path - MATLAB rmpath - MathWorks
This MATLAB function removes the specified folder from the search path. ... Name of folder to remove from the search path, specified as...
Read more >
Using OpenAPI and Swagger UI - Quarkus
This guide explains how your Quarkus application can expose its API description through an OpenAPI specification and how you can test it via...
Read more >
importlib — The implementation of import — Python 3.11.1 ...
A legacy method for finding a loader for the specified module. If this is a top-level import, path will be None . Otherwise,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found