Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/modern-coats-sink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": major
---

Added AsyncAPI support to the `stats` command.
77 changes: 55 additions & 22 deletions packages/cli/src/commands/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ import {
getTypes,
normalizeVisitors,
walkDocument,
Stats,
StatsOAS,
StatsAsync2,
StatsAsync3,
bundle,
logger,
} from '@redocly/openapi-core';
import type { StatsAccumulator, StatsName, WalkContext, OutputFormat } from '@redocly/openapi-core';
import type {
OASStatsAccumulator,
AsyncAPIStatsAccumulator,
WalkContext,
OutputFormat,
} from '@redocly/openapi-core';
import * as colors from 'colorette';
import { performance } from 'perf_hooks';

import type { VerifyConfigOptions } from '../types.js';
import { getFallbackApisOrExit, printExecutionTime } from '../utils/miscellaneous.js';
import type { CommandArgs } from '../wrapper.js';

const statsAccumulator: StatsAccumulator = {
const createOASStatsAccumulator = (): OASStatsAccumulator => ({
refs: { metric: '🚗 References', total: 0, color: 'red', items: new Set() },
externalDocs: { metric: '📦 External Documents', total: 0, color: 'magenta' },
schemas: { metric: '📈 Schemas', total: 0, color: 'white' },
Expand All @@ -28,44 +35,52 @@ const statsAccumulator: StatsAccumulator = {
webhooks: { metric: '🎣 Webhooks', total: 0, color: 'green' },
operations: { metric: '👷 Operations', total: 0, color: 'yellow' },
tags: { metric: '🔖 Tags', total: 0, color: 'white', items: new Set() },
};
});

function printStatsStylish(statsAccumulator: StatsAccumulator) {
for (const node in statsAccumulator) {
const { metric, total, color } = statsAccumulator[node as StatsName];
const createAsyncAPIStatsAccumulator = (): AsyncAPIStatsAccumulator => ({
refs: { metric: '🚗 References', total: 0, color: 'red', items: new Set() },
externalDocs: { metric: '📦 External Documents', total: 0, color: 'magenta' },
schemas: { metric: '📈 Schemas', total: 0, color: 'white' },
parameters: { metric: '👉 Parameters', total: 0, color: 'yellow', items: new Set() },
channels: { metric: '📡 Channels', total: 0, color: 'green' },
operations: { metric: '👷 Operations', total: 0, color: 'yellow' },
tags: { metric: '🔖 Tags', total: 0, color: 'white', items: new Set() },
});

logger.output(colors[color](`${metric}: ${total} \n`));
function printStatsStylish(statsAccumulator: OASStatsAccumulator | AsyncAPIStatsAccumulator) {
for (const node in statsAccumulator) {
const stat = statsAccumulator[node as keyof typeof statsAccumulator];
const { metric, total, color } = stat;
const colorFn = colors[color as keyof typeof colors] as (text: string) => string;
logger.output(colorFn(`${metric}: ${total} \n`));
}
}

function printStatsJson(statsAccumulator: StatsAccumulator) {
function printStatsJson(statsAccumulator: OASStatsAccumulator | AsyncAPIStatsAccumulator) {
const json: any = {};
for (const key of Object.keys(statsAccumulator)) {
const stat = statsAccumulator[key as keyof typeof statsAccumulator];
json[key] = {
metric: statsAccumulator[key as StatsName].metric,
total: statsAccumulator[key as StatsName].total,
metric: stat.metric,
total: stat.total,
};
}

logger.output(JSON.stringify(json, null, 2));
}

function printStatsMarkdown(statsAccumulator: StatsAccumulator) {
function printStatsMarkdown(statsAccumulator: OASStatsAccumulator | AsyncAPIStatsAccumulator) {
let output = '| Feature | Count |\n| --- | --- |\n';
for (const key of Object.keys(statsAccumulator)) {
output +=
'| ' +
statsAccumulator[key as StatsName].metric +
' | ' +
statsAccumulator[key as StatsName].total +
' |\n';
const stat = statsAccumulator[key as keyof typeof statsAccumulator];
output += '| ' + stat.metric + ' | ' + stat.total + ' |\n';
}

logger.output(output);
}

function printStats(
statsAccumulator: StatsAccumulator,
statsAccumulator: OASStatsAccumulator | AsyncAPIStatsAccumulator,
api: string,
startedAt: number,
format: string
Expand Down Expand Up @@ -100,6 +115,24 @@ export async function handleStats({ argv, config, collectSpecData }: CommandArgs
const specVersion = detectSpec(document.parsed);
const types = normalizeTypes(config.extendTypes(getTypes(specVersion), specVersion), config);

const statsAccumulatorOAS = createOASStatsAccumulator();
const statsAccumulatorAsync2 = createAsyncAPIStatsAccumulator();
const statsAccumulatorAsync3 = createAsyncAPIStatsAccumulator();

const statsVisitor =
specVersion === 'async2'
? StatsAsync2(statsAccumulatorAsync2)
: specVersion === 'async3'
? StatsAsync3(statsAccumulatorAsync3)
: StatsOAS(statsAccumulatorOAS);

const statsAccumulator =
specVersion === 'async2'
? statsAccumulatorAsync2
: specVersion === 'async3'
? statsAccumulatorAsync3
: statsAccumulatorOAS;

const startedAt = performance.now();
const ctx: WalkContext = {
problems: [],
Expand All @@ -114,12 +147,12 @@ export async function handleStats({ argv, config, collectSpecData }: CommandArgs
externalRefResolver,
});

const statsVisitor = normalizeVisitors(
const normalizedStatsVisitor = normalizeVisitors(
[
{
severity: 'warn',
ruleId: 'stats',
visitor: Stats(statsAccumulator),
visitor: statsVisitor,
},
],
types
Expand All @@ -128,7 +161,7 @@ export async function handleStats({ argv, config, collectSpecData }: CommandArgs
walkDocument({
document,
rootType: types.Root,
normalizedVisitors: statsVisitor,
normalizedVisitors: normalizedStatsVisitor,
resolvedRefMap,
ctx,
});
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export { OpenRpcTypes } from './types/openrpc.js';
export { ConfigTypes, createConfigTypes } from './types/redocly-yaml.js';
export { createEntityTypes } from './types/entity.js';
export { normalizeTypes, type NormalizedNodeType, type NodeType } from './types/index.js';
export { Stats } from './rules/other/stats.js';
export { StatsOAS, StatsAsync2, StatsAsync3 } from './rules/other/stats.js';
export {
loadConfig,
loadIgnoreConfig,
Expand Down Expand Up @@ -159,4 +159,9 @@ export type {
ExtendedSecurity,
ResolvedSecurity,
} from './typings/arazzo.js';
export type { StatsAccumulator, StatsName } from './typings/common.js';
export type {
StatsAccumulator,
OASStatsAccumulator,
AsyncAPIStatsAccumulator,
StatsName,
} from './typings/common.js';
138 changes: 128 additions & 10 deletions packages/core/src/rules/other/stats.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { StatsAccumulator } from '../../typings/common.js';
import type { OASStatsAccumulator, AsyncAPIStatsAccumulator } from '../../typings/common.js';
import type { Oas3Parameter, OasRef, Oas3Tag, Oas3_2Tag } from '../../typings/openapi.js';
import type { Oas2Parameter } from '../../typings/swagger.js';

export const Stats = (statsAccumulator: StatsAccumulator) => {
export const StatsOAS = (statsAccumulator: OASStatsAccumulator) => {
return {
ExternalDocs: {
leave() {
Expand All @@ -24,14 +24,6 @@ export const Stats = (statsAccumulator: StatsAccumulator) => {
statsAccumulator.links.items!.add(link.operationId);
},
},
Root: {
Copy link
Copy Markdown
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.

leave() {
statsAccumulator.parameters.total = statsAccumulator.parameters.items!.size;
statsAccumulator.refs.total = statsAccumulator.refs.items!.size;
statsAccumulator.links.total = statsAccumulator.links.items!.size;
statsAccumulator.tags.total = statsAccumulator.tags.items!.size;
},
},
WebhooksMap: {
Operation: {
leave(operation: any) {
Expand Down Expand Up @@ -73,5 +65,131 @@ export const Stats = (statsAccumulator: StatsAccumulator) => {
},
},
},
Root: {
leave() {
statsAccumulator.parameters.total = statsAccumulator.parameters.items!.size;
statsAccumulator.refs.total = statsAccumulator.refs.items!.size;
statsAccumulator.links.total = statsAccumulator.links.items!.size;
statsAccumulator.tags.total = statsAccumulator.tags.items!.size;
},
},
};
};

export const StatsAsync2 = (statsAccumulator: AsyncAPIStatsAccumulator) => {
return {
ExternalDocs: {
leave() {
statsAccumulator.externalDocs.total++;
},
},
ref: {
enter(ref: OasRef) {
statsAccumulator.refs.items!.add(ref['$ref']);
},
},
Tag: {
leave(tag: Oas3Tag | Oas3_2Tag) {
statsAccumulator.tags.items!.add(tag.name);
},
},
ChannelMap: {
Channel: {
leave() {
statsAccumulator.channels.total++;
},
Operation: {
leave(operation: any) {
statsAccumulator.operations.total++;
if (operation.tags) {
for (const tag of operation.tags) {
statsAccumulator.tags.items!.add(tag);
}
}
},
},
Parameter: {
leave(parameter: any) {
if (parameter.name) {
statsAccumulator.parameters.items!.add(parameter.name);
}
},
},
},
},
NamedSchemas: {
Schema: {
leave() {
statsAccumulator.schemas.total++;
},
},
},
Root: {
leave() {
statsAccumulator.parameters.total = statsAccumulator.parameters.items!.size;
statsAccumulator.refs.total = statsAccumulator.refs.items!.size;
statsAccumulator.tags.total = statsAccumulator.tags.items!.size;
},
},
};
};

export const StatsAsync3 = (statsAccumulator: AsyncAPIStatsAccumulator) => {
return {
ExternalDocs: {
leave() {
statsAccumulator.externalDocs.total++;
},
},
ref: {
enter(ref: OasRef) {
statsAccumulator.refs.items!.add(ref['$ref']);
},
},
Tag: {
leave(tag: Oas3Tag | Oas3_2Tag) {
statsAccumulator.tags.items!.add(tag.name);
},
},
NamedChannels: {
Channel: {
leave() {
statsAccumulator.channels.total++;
},
Parameter: {
leave(parameter: any) {
if (parameter.name) {
statsAccumulator.parameters.items!.add(parameter.name);
}
},
},
},
},
NamedOperations: {
Operation: {
leave(operation: any) {
statsAccumulator.operations.total++;
if (operation.tags) {
for (const tag of operation.tags) {
statsAccumulator.tags.items!.add(tag);
}
}
},
},
},
NamedSchemas: {
Schema: {
leave() {
statsAccumulator.schemas.total++;
},
},
},
Root: {
leave() {
statsAccumulator.parameters.total = statsAccumulator.parameters.items!.size;
statsAccumulator.refs.total = statsAccumulator.refs.items!.size;
statsAccumulator.tags.total = statsAccumulator.tags.items!.size;
},
},
};
};
17 changes: 15 additions & 2 deletions packages/core/src/typings/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface StatsRow {
items?: Set<string>;
}

export type StatsName =
export type OASStatsName =
| 'operations'
| 'refs'
| 'tags'
Expand All @@ -15,4 +15,17 @@ export type StatsName =
| 'schemas'
| 'webhooks'
| 'parameters';
export type StatsAccumulator = Record<StatsName, StatsRow>;

export type AsyncAPIStatsName =
| 'operations'
| 'refs'
| 'tags'
| 'externalDocs'
| 'channels'
| 'schemas'
| 'parameters';

export type StatsName = OASStatsName | AsyncAPIStatsName;
export type OASStatsAccumulator = Record<OASStatsName, StatsRow>;
export type AsyncAPIStatsAccumulator = Record<AsyncAPIStatsName, StatsRow>;
export type StatsAccumulator = OASStatsAccumulator | AsyncAPIStatsAccumulator;
21 changes: 21 additions & 0 deletions tests/e2e/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,5 +966,26 @@ describe('E2E', () => {
const result = getCommandOutput(args, { testPath });
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt'));
});

test('stats should support AsyncAPI 2.x (stylish format)', async () => {
const testPath = join(folderPath, 'stats-async2-stylish');
const args = getParams(indexEntryPoint, ['stats', 'async.yaml']);
const result = getCommandOutput(args, { testPath });
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt'));
});

test('stats should support AsyncAPI 2.x (JSON format)', async () => {
const testPath = join(folderPath, 'stats-async2-json');
const args = getParams(indexEntryPoint, ['stats', 'async.yaml', '--format=json']);
const result = getCommandOutput(args, { testPath });
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt'));
});

test('stats should support AsyncAPI 3.x (stylish format)', async () => {
const testPath = join(folderPath, 'stats-async3-stylish');
const args = getParams(indexEntryPoint, ['stats', 'asyncapi3.yaml']);
const result = getCommandOutput(args, { testPath });
await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt'));
});
});
});
Loading
Loading