Skip to content

Commit

Permalink
feat(toolkit): enforce codes at compile time
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgrain committed Jan 13, 2025
1 parent 0a893b6 commit 8330271
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 134 deletions.
38 changes: 17 additions & 21 deletions packages/aws-cdk/lib/logging.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as util from 'util';
import { IoMessageLevel, IoMessage, CliIoHost, validateMessageCode } from './toolkit/cli-io-host';
import { IoMessageLevel, IoMessage, CliIoHost, IoMessageSpecificCode, IoMessageCode, IoMessageCodeCategory, IoCodeLevel } from './toolkit/cli-io-host';
import * as chalk from './util/cdk-chalk';

// Corking mechanism
Expand Down Expand Up @@ -48,7 +48,7 @@ export async function withCorkedLogging<T>(block: () => Promise<T>): Promise<T>
if (CORK_COUNTER === 0) {
// Process each buffered message through notify
for (const ioMessage of logBuffer) {
void CliIoHost.getIoHost().notify(ioMessage);
await CliIoHost.getIoHost().notify(ioMessage);
}
logBuffer.splice(0);
}
Expand All @@ -74,7 +74,7 @@ interface LogOptions {
* @pattern [A-Z]+_[0-2][0-9]{3}
* @default TOOLKIT_[0/1/2]000
*/
readonly code: string;
readonly code: IoMessageCode;
}

/**
Expand All @@ -86,8 +86,6 @@ function log(options: LogOptions) {
return;
}

validateMessageCode(options.code, options.level);

const ioMessage: IoMessage = {
level: options.level,
message: options.message,
Expand All @@ -113,7 +111,7 @@ function log(options: LogOptions) {
function formatLogMessage(
level: IoMessageLevel,
forceStdout = false,
input: LogInput,
input: LogInput<IoCodeLevel>,
style?: (str: string) => string,
...args: unknown[]
): void {
Expand All @@ -128,8 +126,6 @@ function formatLogMessage(
// Apply style if provided
const finalMessage = style ? style(formattedMessage) : formattedMessage;

validateMessageCode(code, level);

log({
level,
message: finalMessage,
Expand All @@ -138,27 +134,27 @@ function formatLogMessage(
});
}

function getDefaultCode(level: IoMessageLevel, category: string = 'TOOLKIT'): string {
function getDefaultCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT'): IoMessageCode {
const levelIndicator = level === 'error' ? 'E' :
level === 'warn' ? 'W' :
'I';
return `CDK_${category}_${levelIndicator}000`;
return `CDK_${category}_${levelIndicator}0000`;
}

// Type for the object parameter style
interface LogParams {
interface LogParams<L extends IoCodeLevel> {
/**
* @see {@link IoMessage.code}
*/
readonly code?: string;
readonly code?: IoMessageSpecificCode<L>;
/**
* @see {@link IoMessage.message}
*/
readonly message: string;
}

// Type for the exported log function arguments
type LogInput = string | LogParams;
type LogInput<L extends IoCodeLevel> = string | LogParams<L>;

// Exported logging functions. If any additional logging functionality is required, it should be added as
// a new logging function here.
Expand All @@ -174,7 +170,7 @@ type LogInput = string | LogParams;
* error({ message: 'operation failed: %s', code: 'CDK_SDK_E001' }, e) // specifies error code `CDK_SDK_E001`
* ```
*/
export const error = (input: LogInput, ...args: unknown[]) => {
export const error = (input: LogInput<'E'>, ...args: unknown[]) => {
return formatLogMessage('error', false, input, undefined, ...args);
};

Expand All @@ -189,7 +185,7 @@ export const error = (input: LogInput, ...args: unknown[]) => {
* warning({ message: 'deprected feature: %s', code: 'CDK_SDK_W001' }, message) // specifies warning code `CDK_SDK_W001`
* ```
*/
export const warning = (input: LogInput, ...args: unknown[]) => {
export const warning = (input: LogInput<'W'>, ...args: unknown[]) => {
return formatLogMessage('warn', false, input, undefined, ...args);
};

Expand All @@ -204,7 +200,7 @@ export const warning = (input: LogInput, ...args: unknown[]) => {
* info({ message: 'processing: %s', code: 'CDK_TOOLKIT_I001' }, message) // specifies info code `CDK_TOOLKIT_I001`
* ```
*/
export const info = (input: LogInput, ...args: unknown[]) => {
export const info = (input: LogInput<'I'>, ...args: unknown[]) => {
return formatLogMessage('info', false, input, undefined, ...args);
};

Expand All @@ -219,7 +215,7 @@ export const info = (input: LogInput, ...args: unknown[]) => {
* data({ message: 'stats: %j', code: 'CDK_DATA_I001' }, stats) // specifies info code `CDK_DATA_I001`
* ```
*/
export const data = (input: LogInput, ...args: unknown[]) => {
export const data = (input: LogInput<'I'>, ...args: unknown[]) => {
return formatLogMessage('info', true, input, undefined, ...args);
};

Expand All @@ -234,7 +230,7 @@ export const data = (input: LogInput, ...args: unknown[]) => {
* debug({ message: 'ratio: %d%%', code: 'CDK_TOOLKIT_I001' }, ratio) // specifies info code `CDK_TOOLKIT_I001`
* ```
*/
export const debug = (input: LogInput, ...args: unknown[]) => {
export const debug = (input: LogInput<'I'>, ...args: unknown[]) => {
return formatLogMessage('debug', false, input, undefined, ...args);
};

Expand All @@ -249,7 +245,7 @@ export const debug = (input: LogInput, ...args: unknown[]) => {
* trace({ message: 'method: %s', code: 'CDK_TOOLKIT_I001' }, name) // specifies info code `CDK_TOOLKIT_I001`
* ```
*/
export const trace = (input: LogInput, ...args: unknown[]) => {
export const trace = (input: LogInput<'I'>, ...args: unknown[]) => {
return formatLogMessage('trace', false, input, undefined, ...args);
};

Expand All @@ -264,7 +260,7 @@ export const trace = (input: LogInput, ...args: unknown[]) => {
* success({ message: 'items: %d', code: 'CDK_TOOLKIT_I001' }, count) // specifies info code `CDK_TOOLKIT_I001`
* ```
*/
export const success = (input: LogInput, ...args: unknown[]) => {
export const success = (input: LogInput<'I'>, ...args: unknown[]) => {
return formatLogMessage('info', false, input, chalk.green, ...args);
};

Expand All @@ -279,6 +275,6 @@ export const success = (input: LogInput, ...args: unknown[]) => {
* highlight({ message: 'notice: %s', code: 'CDK_TOOLKIT_I001' }, msg) // specifies info code `CDK_TOOLKIT_I001`
* ```
*/
export const highlight = (input: LogInput, ...args: unknown[]) => {
export const highlight = (input: LogInput<'I'>, ...args: unknown[]) => {
return formatLogMessage('info', false, input, chalk.bold, ...args);
};
68 changes: 18 additions & 50 deletions packages/aws-cdk/lib/toolkit/cli-io-host.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as chalk from '../util/cdk-chalk';

export type IoMessageCodeCategory = 'TOOLKIT' | 'SDK' | 'ASSETS';
export type IoCodeLevel = 'E' | 'W' | 'I';
export type IoMessageSpecificCode<L extends IoCodeLevel> = `CDK_${IoMessageCodeCategory}_${L}${number}${number}${number}${number}`;
export type IoMessageCode = IoMessageSpecificCode<IoCodeLevel>;

/**
* Basic message structure for toolkit notifications.
* Messages are emitted by the toolkit and handled by the IoHost.
Expand All @@ -18,7 +23,7 @@ export interface IoMessage {
/**
* The action that triggered the message.
*/
readonly action: IoAction;
readonly action: ToolkitAction;

/**
* A short message code uniquely identifying a message type using the format CDK_[CATEGORY]_[E/W/I][000-999].
Expand All @@ -36,7 +41,7 @@ export interface IoMessage {
* 'CDK_SDK_W023' // valid: specific sdk warning message
* ```
*/
readonly code: string;
readonly code: IoMessageCode;

/**
* The message text.
Expand All @@ -57,7 +62,7 @@ export type IoMessageLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
/**
* The current action being performed by the CLI. 'none' represents the absence of an action.
*/
export type IoAction = 'synth' | 'list' | 'deploy' | 'destroy' | 'none';
export type ToolkitAction = 'synth' | 'list' | 'deploy' | 'destroy' | 'none';

/**
* A simple IO host for the CLI that writes messages to the console.
Expand All @@ -73,10 +78,15 @@ export class CliIoHost {
return CliIoHost.instance;
}

/**
* Singleton instance of the CliIoHost
*/
private static instance: CliIoHost | undefined;

/**
* Determines which output stream to use based on log level and configuration.
*/
public static getStream(level: IoMessageLevel, forceStdout: boolean) {
private static getStream(level: IoMessageLevel, forceStdout: boolean) {
// For legacy purposes all log streams are written to stderr by default, unless
// specified otherwise, by passing `forceStdout`, which is used by the `data()` logging function, or
// if the CDK is running in a CI environment. This is because some CI environments will immediately
Expand All @@ -89,11 +99,6 @@ export class CliIoHost {
return this.ci ? process.stdout : process.stderr;
}

/**
* Singleton instance of the CliIoHost
*/
private static instance: CliIoHost | undefined;

/**
* Whether the host should apply chalk styles to messages. Defaults to false if the host is not running in a TTY.
*
Expand All @@ -109,20 +114,20 @@ export class CliIoHost {
private ci: boolean;

/**
* the current {@link IoAction} set by the CLI.
* the current {@link ToolkitAction} set by the CLI.
*/
private currentAction: IoAction | undefined;
private currentAction: ToolkitAction | undefined;

private constructor() {
this.isTTY = process.stdout.isTTY ?? false;
this.ci = false;
}

public static get currentAction(): IoAction | undefined {
public static get currentAction(): ToolkitAction | undefined {
return CliIoHost.getIoHost().currentAction;
}

public static set currentAction(action: IoAction) {
public static set currentAction(action: ToolkitAction) {
CliIoHost.getIoHost().currentAction = action;
}

Expand Down Expand Up @@ -186,43 +191,6 @@ export class CliIoHost {
}
}

/**
* Validates that a message code follows the required format:
* CDK_[CATEGORY]_[E/W/I][000-999]
*
* Examples:
* - CDK_ASSETS_E005 (specific asset error)
* - CDK_SDK_W001 (specific SDK warning)
* - CDK_TOOLKIT_I000 (generic toolkit info)
*
* @param code The message code to validate
* @param level The message level (used to validate level indicator matches)
* @throws Error if the code format is invalid
*/
export function validateMessageCode(code: string, level: IoMessageLevel): void {
const MESSAGE_CODE_PATTERN = /^CDK_[A-Z]+_[EWI][0-9]{3}$/;
if (!MESSAGE_CODE_PATTERN.test(code)) {
throw new Error(
`Invalid message code format: "${code}". ` +
'Code must match pattern: CDK_[CATEGORY]_[E/W/I][000-999]',
);
}

// Extract level indicator from code (E/W/I after second underscore)
const levelIndicator = code.split('_')[2][0];

// Validate level indicator matches message level
const expectedIndicator = level === 'error' ? 'E' :
level === 'warn' ? 'W' : 'I';

if (levelIndicator !== expectedIndicator) {
throw new Error(
`Message code level indicator '${levelIndicator}' does not match message level '${level}'. ` +
`Expected '${expectedIndicator}'`,
);
}
}

export const styleMap: Record<IoMessageLevel, (str: string) => string> = {
error: chalk.red,
warn: chalk.yellow,
Expand Down
Loading

0 comments on commit 8330271

Please sign in to comment.