Skip to content

feat: add AsyncAPI support in the stats command#2553

Merged
DmitryAnansky merged 10 commits intoRedocly:mainfrom
tibisabau:feat/add-asyncapi-support-stats
Feb 26, 2026
Merged

feat: add AsyncAPI support in the stats command#2553
DmitryAnansky merged 10 commits intoRedocly:mainfrom
tibisabau:feat/add-asyncapi-support-stats

Conversation

@tibisabau
Copy link
Contributor

What/Why/How?

Added AsyncAPI 2.x and 3.x support to the stats command to provide statistics for AsyncAPI documents, similar to existing OpenAPI support. The stats command now counts channels, operations, schemas, references, tags, parameters, and external docs for AsyncAPI specifications.

Why: Since AsyncAPI support was added for lint and bundle commands, the stats command should support it as well for consistency and completeness.

How: Extended the Stats visitor in packages/core/src/rules/other/stats.ts to handle AsyncAPI-specific node types (ChannelMap, NamedChannels, NamedOperations) and map them to appropriate metrics (channels → pathItems, operations → operations).

Reference

Closes #2353.

Testing

  • Added 3 new E2E test cases covering AsyncAPI 2.x and 3.x stats in stylish and JSON formats
  • All tests pass successfully with generated snapshots
  • Verified stats output for both AsyncAPI versions:
    • AsyncAPI 2.x: 4 channels, 4 operations, 18 references, 17 schemas, 12 external docs, 11 tags
    • AsyncAPI 3.x: 1 channel, 1 operation, 2 references

Screenshots (optional)

N/A

Check yourself

  • Code changed? - Tested with Redoc/Realm/Reunite (internal)
  • All new/updated code is covered by tests
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

@tibisabau tibisabau requested review from a team as code owners February 10, 2026 15:38
@changeset-bot
Copy link

changeset-bot bot commented Feb 10, 2026

🦋 Changeset detected

Latest commit: de85bd9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@redocly/openapi-core Major
@redocly/cli Major
@redocly/respect-core Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@@ -1 +1 @@
../../../../resources/pets.yaml No newline at end of file
../../../../resources/pets.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like nothing changed here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you revert this change? It seems it was accidental.


function printStatsStylish(statsAccumulator: StatsAccumulator) {
// Determine which stats are relevant for a given spec version
function isStatRelevant(stat: StatsName, specVersion: SpecVersion): boolean {
Copy link
Collaborator

@tatomyr tatomyr Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The walker should handle this by itself. To get different output for different specs, please use different statsAccumulator presets. (If I get your intention correctly.)

},
});

export const Stats = (statsAccumulator: StatsAccumulator, specVersion?: SpecVersion) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest creating different Stats rules for different specifications/version: SpecOAS, SpecAsync2, SpecAsync3 (for now). This will help to avoid collisions and later on will ease our way when migrating to a more robust solution (ultimately, the stats should use the same approach as lint and bundle, but it requires a bit of a preparation on our side, so I'd go with this PR first, and then we'll implement what's needed on our side).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chances are, we'll need separate statsAccumulators too to be able to scope the stats we're showing to the spec.

statsAccumulator.externalDocs.total++;
},
// Common visitor for all specs (refs, tags, externalDocs, links)
const getCommonVisitor = (statsAccumulator: StatsAccumulator) => ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use common visitors, just duplicate what's needed. It's better for understanding the actual structure of the data we're trying to get.

Comment on lines +135 to +136
// Handle refs in messages array (AsyncAPI 3 specific)
// Note: The ref visitor may not catch refs in arrays due to walker limitations,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the limitation? If there's any that doesn't allow us to walk here, it should be fixed.

@tibisabau tibisabau requested a review from tatomyr February 24, 2026 10:05
Copy link
Collaborator

@tatomyr tatomyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already much better, thank you! One more push needed though--left a couple of comments.

parameters: { metric: '👉 Parameters', total: 0, color: 'yellow', items: new Set() },
links: { metric: '🔗 Links', total: 0, color: 'cyan', items: new Set() },
pathItems: { metric: '🔀 Path Items', total: 0, color: 'green' },
channels: { metric: '📡 Channels', total: 0, color: 'green' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually the point of the split for the accumulators: let's declare only the specific fields there.

Suggested change
channels: { metric: '📡 Channels', total: 0, color: 'green' },

Comment on lines +43 to +44
links: { metric: '🔗 Links', total: 0, color: 'cyan', items: new Set() },
pathItems: { metric: '🔀 Path Items', total: 0, color: 'green' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no links and pathItems in AsyncAPI (and probably other fields too).

});

// Stats to show for OpenAPI
const oasStatsToShow: Set<StatsName> = new Set([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just put only those things into accumulator objects (and then check if they are defined why formatting the output). This will simplify the code as you won't have to pass statsToShow down to the printStats... functions.

Comment on lines +151 to +158
const isAsyncAPI = specVersion === 'async2' || specVersion === 'async3';
const statsAccumulator = isAsyncAPI
? createAsyncAPIStatsAccumulator()
: createOASStatsAccumulator();
const statsToShow = isAsyncAPI ? asyncStatsToShow : oasStatsToShow;

const statsVisitorFn =
specVersion === 'async2' ? StatsAsync2 : specVersion === 'async3' ? StatsAsync3 : StatsOAS;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be simplified like so?

Suggested change
const isAsyncAPI = specVersion === 'async2' || specVersion === 'async3';
const statsAccumulator = isAsyncAPI
? createAsyncAPIStatsAccumulator()
: createOASStatsAccumulator();
const statsToShow = isAsyncAPI ? asyncStatsToShow : oasStatsToShow;
const statsVisitorFn =
specVersion === 'async2' ? StatsAsync2 : specVersion === 'async3' ? StatsAsync3 : StatsOAS;
const statsVisitor =
specVersion === 'async2' ? StatsAsync2(statsAccumulatorAsync2) : specVersion === 'async3' ? StatsAsync3(statsAccumulatorAsync3) : StatsOAS(statsAccumulatorOAS);

| 'tags'
| 'externalDocs'
| 'pathItems'
| 'channels'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have separate types for different specs?

@tatomyr
Copy link
Collaborator

tatomyr commented Feb 24, 2026

And please rebase your branch against main--there were some changes that might conflict with yours.

@tibisabau tibisabau force-pushed the feat/add-asyncapi-support-stats branch from c5fcbbf to 4d9b3f6 Compare February 24, 2026 12:54
@tibisabau tibisabau requested a review from tatomyr February 24, 2026 12:55
ref: {
enter(ref: OasRef) {
statsAccumulator.refs.items!.add(ref['$ref']);
statsAccumulator.refs?.items!.add(ref['$ref']);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need the optional chaining here? There are always items in the refs field unless I'm missing something. Just use the appropriate type. You can even infer the keys from the accumulator objects if needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for if (statsAccumulator.parameters) { and other precautions.

statsAccumulator.links.items!.add(link.operationId);
},
},
Root: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is changing the order really necessary? If not, please revert that so it's easier to read the diffs.

@@ -1 +1 @@
../../../../resources/pets.yaml No newline at end of file
../../../../resources/pets.yaml
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you revert this change? It seems it was accidental.

@@ -0,0 +1,606 @@
asyncapi: '2.6.0'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clean up test examples (preferably leaving a minimalistic sample) so it's easier to track the actual statistics?

const json: any = {};
for (const key of Object.keys(statsAccumulator)) {
const stat = statsAccumulator[key as keyof StatsAccumulator];
if (!stat) continue;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What could be the case when there's no stat?

@tibisabau tibisabau changed the title feat: Add AsyncAPI support in the stats command feat: add AsyncAPI support in the stats command Feb 24, 2026
@tibisabau tibisabau requested a review from tatomyr February 24, 2026 19:42
@tatomyr
Copy link
Collaborator

tatomyr commented Feb 25, 2026

The coverage threshold still fails. Please relax it a bit in the config.
Overall looks good to me. Let's wait for the tech writer's approval.

Copy link
Contributor

@DmitryAnansky DmitryAnansky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a small note regarding the comments in another file.

@DmitryAnansky DmitryAnansky merged commit 5ab3de9 into Redocly:main Feb 26, 2026
36 checks passed
@DmitryAnansky
Copy link
Contributor

DmitryAnansky commented Feb 26, 2026

@tibisabau
Thank you for your contribution!
The changes will be included in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add AsyncAPI support in the stats command

4 participants