blob: c68e4ef778df0d48c22c162f357a2edf38ab75b6 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Tool for generating Exceptions to wrap JSON-RPC 2.0 error codes.
///
/// In JSON-RPC 2.0 error messages carries:
/// * A numeric error code,
/// * A message, and,
/// * JSON data.
///
/// The specification provides a short list of _standard error codes_ beyond
/// those applications have to define their own. For the communication between
/// `package:dartpad` the _Web Worker_ and the _sandboxed iframe_ we are using
/// JSON RPC 2.0, and the list of error codes is specified in:
///
/// pkg/dartpad/lib/src/exceptions.yaml
///
/// This script will read this YAML document and generate:
///
/// pkg/dartpad/lib/src/exceptions.dart
///
/// Containing an Exception class for each defined error code.
/// Furthermore, this script will update the documented table of errors in:
///
/// pkg/dartpad/doc/worker-protocol.md
///
/// Run this script after updating `exceptions.yaml` and take care that some
/// error codes (as noted in `exceptions.yaml`) must be manually sync'ed into
/// source files.
library;
import 'dart:io';
import 'dart:isolate';
import 'package:yaml/yaml.dart';
void main() async {
// Resolve paths
final yamlUri = Isolate.resolvePackageUriSync(
Uri.parse('package:dartpad/src/exceptions.yaml'),
);
final dartUri = Isolate.resolvePackageUriSync(
Uri.parse('package:dartpad/src/exceptions.dart'),
);
if (yamlUri == null || dartUri == null) {
print('Error: Could not resolve package:dartpad/src/exceptions.dart');
exit(1);
}
// package:dartpad/src/ resolves to lib/src/, so doc/ is at lib/../doc/
final docUri = yamlUri.resolve('../../doc/worker-protocol.md');
final docFile = File.fromUri(docUri);
final dartFile = File.fromUri(dartUri);
// Parse YAML
final yamlString = File.fromUri(yamlUri).readAsStringSync();
final yamlDoc = loadYaml(yamlString) as YamlMap;
final groups = <ExceptionCodeGroup>[];
for (final entry in yamlDoc['groups'] as YamlList) {
final g = entry as YamlMap;
final groupBaseName = g['name'] as String;
final description = g['description'] as String;
final codes = <ExceptionCode>[];
for (final entry in (g['codes'] as YamlMap).entries) {
final code = entry.key as int;
final details = entry.value as YamlMap;
final baseName = details['name'] as String;
final className = '${baseName}Exception';
final enumName = baseName[0].toLowerCase() + baseName.substring(1);
codes.add(
ExceptionCode(
code: code,
className: className,
enumName: enumName,
description: (details['description'] as String).trim(),
),
);
}
// Sort by code to guarantee deterministic output
codes.sort((a, b) => a.code.compareTo(b.code));
groups.add(
ExceptionCodeGroup(
className: '${groupBaseName}Exception',
description: description,
codes: codes,
),
);
}
// 3. Generate Dart File
_generateDartFile(dartFile, groups);
// 4. Update Markdown File
_updateMarkdownFile(docFile, groups);
print(
'Successfully generated exceptions.dart and updated worker-protocol.md',
);
}
final class ExceptionCodeGroup {
final String className;
final String description;
final List<ExceptionCode> codes;
ExceptionCodeGroup({
required this.className,
required this.description,
required this.codes,
});
}
final class ExceptionCode {
final int code;
final String className;
final String enumName;
final String description;
ExceptionCode({
required this.code,
required this.className,
required this.enumName,
required this.description,
});
}
void _generateDartFile(File dartFile, List<ExceptionCodeGroup> groups) {
final buffer = StringBuffer();
final allCodes = groups.expand((g) => g.codes).toList();
buffer.writeln('''
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Generated code. Do not modify.
//
// This file is generated by tool/generate_exceptions.dart.
// To modify these exceptions, edit lib/src/exceptions.yaml and re-run.
import 'package:json_rpc_2/json_rpc_2.dart';
/// JSON-RPC 2.0 error codes that may be returned by worker.
enum _ErrorCode {''');
for (final c in allCodes) {
buffer.writeln(' ${c.enumName}(${c.code}),');
}
buffer.writeln('''
;
const _ErrorCode(this.code);
final int code;
}
typedef _MakeException = Exception Function(String message, {Object? data});
/// Registry of all DartPadException constructors for use by
/// [rethrowAsDartPadException].
final _exceptionRegistry = <_ErrorCode, _MakeException>{''');
for (final c in allCodes) {
buffer.writeln(' .${c.enumName}: ${c.className}.new,');
}
buffer.writeln('''
}.map((key, value) => MapEntry(key.code, value));
/// Throw a [DartPadException] for [RpcException] depending on error code.
Never rethrowAsDartPadException(RpcException e) {
final f = _exceptionRegistry[e.code];
if (f != null) {
throw f(e.message, data: e.data);
}
throw e;
}
final class DartPadException extends RpcException {
DartPadException._(String message, _ErrorCode e, {super.data})
: super(e.code, message);
@override
String toString() => 'DartPadException(\$code: \$message)';
}
''');
for (final g in groups) {
buffer.writeln('''
${wrapDescription(g.description)}
abstract final class ${g.className} extends DartPadException {
${g.className}._(super.message, super.e, {super.data}) : super._();
@override String toString() =>
'${g.className}(\$code: \$message)';
}
''');
for (final c in g.codes) {
buffer.writeln('''
${wrapDescription(c.description)}
final class ${c.className} extends ${g.className} {
${c.className}(String message, {super.data})
: super._(message, .${c.enumName});
@override String toString() =>
'${c.className}(\$code: \$message)';
}
''');
}
}
dartFile.writeAsStringSync(buffer.toString());
// Run `dart format` to ensure the generated file looks perfect
Process.runSync(Platform.resolvedExecutable, ['format', dartFile.path]);
}
void _updateMarkdownFile(File docFile, List<ExceptionCodeGroup> groups) {
if (!docFile.existsSync()) {
print('Warning: Markdown file not found at ${docFile.path}');
return;
}
final allCodes = groups.expand((g) => g.codes).toList();
final content = docFile.readAsStringSync();
final beginMarker = '<!-- BEGIN GENERATED ERROR CODE TABLE -->';
final endMarker = '<!-- END GENERATED ERROR CODE TABLE -->';
final regex = RegExp(
'${RegExp.escape(beginMarker)}(.*?)${RegExp.escape(endMarker)}',
dotAll: true,
);
if (!regex.hasMatch(content)) {
print(
'Warning: Could not find generated table markers in worker-protocol.md',
);
return;
}
final tableBuffer = StringBuffer();
tableBuffer.writeln('\n| Code | Name | Description |');
tableBuffer.writeln('| :--- | :--- | :--- |');
for (final c in allCodes) {
// Replace newlines in description with spaces so it doesn't break the
// markdown table
final safeDescription = c.description.replaceAll('\n', ' ');
tableBuffer.writeln('| ${c.code} | `${c.enumName}` | $safeDescription |');
}
tableBuffer.writeln();
final newContent = content.replaceFirst(
regex,
'$beginMarker${tableBuffer.toString()}$endMarker',
);
docFile.writeAsStringSync(newContent);
}
String wrapDescription(String text, {int width = 80, String prefix = '/// '}) {
final lines = text.split('\n');
final resultBuffer = StringBuffer();
for (var i = 0; i < lines.length; i++) {
final line = lines[i];
if (line.trim().isEmpty) {
// 2. Preserve empty lines (just the prefix)
resultBuffer.write(prefix.trimRight());
} else {
// 3. Wrap the content of the current line
final words = line.split(' ');
var currentLine = prefix;
for (final word in words) {
if ((currentLine + word).length > width) {
resultBuffer.writeln(currentLine.trimRight());
currentLine = '$prefix$word ';
} else {
currentLine += '$word ';
}
}
resultBuffer.write(currentLine.trimRight());
}
if (i < lines.length - 1) {
resultBuffer.writeln();
}
}
return resultBuffer.toString();
}