mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Refactor the code generation process
This commit is contained in:
4
scripts/codegen/.codegen_stamp
Normal file
4
scripts/codegen/.codegen_stamp
Normal file
@@ -0,0 +1,4 @@
|
||||
# Auto-generated by protocol autogen - do not edit manually.
|
||||
# This file tracks input hashes to avoid unnecessary code regeneration.
|
||||
# It should be checked into version control alongside the generated files.
|
||||
COMBINED_HASH=24a9168ac6a450f09fa4e2ab288d06624a368041e91fbc7741101d3565d1e601
|
||||
210
scripts/codegen/generate_ledger_classes.py
Normal file
210
scripts/codegen/generate_ledger_classes.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate C++ wrapper classes for XRP Ledger entry types from ledger_entries.macro.
|
||||
|
||||
This script parses the ledger_entries.macro file and generates type-safe wrapper
|
||||
classes for each ledger entry type, similar to the transaction wrapper classes.
|
||||
|
||||
Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
|
||||
"""
|
||||
|
||||
import io
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import pyparsing as pp
|
||||
|
||||
# Import common utilities
|
||||
from macro_parser_common import (
|
||||
CppCleaner,
|
||||
parse_sfields_macro,
|
||||
parse_field_list,
|
||||
generate_cpp_class,
|
||||
generate_from_template,
|
||||
clear_output_directory,
|
||||
)
|
||||
|
||||
|
||||
def create_ledger_entry_parser():
|
||||
"""Create a pyparsing parser for LEDGER_ENTRY macros.
|
||||
|
||||
This parser extracts the full LEDGER_ENTRY macro call and parses its arguments
|
||||
using pyparsing's nesting-aware delimited list parsing.
|
||||
"""
|
||||
# Match the exact words
|
||||
ledger_entry = pp.Keyword("LEDGER_ENTRY") | pp.Keyword("LEDGER_ENTRY_DUPLICATE")
|
||||
|
||||
# Define nested structures so pyparsing protects them
|
||||
nested_braces = pp.original_text_for(pp.nested_expr("{", "}"))
|
||||
nested_parens = pp.original_text_for(pp.nested_expr("(", ")"))
|
||||
|
||||
# Define standard text (anything that isn't a comma, parens, or braces)
|
||||
plain_text = pp.Word(pp.printables + " \t\n", exclude_chars=",{}()")
|
||||
|
||||
# A single argument is any combination of the above
|
||||
single_arg = pp.Combine(pp.OneOrMore(nested_braces | nested_parens | plain_text))
|
||||
single_arg.set_parse_action(lambda t: t[0].strip())
|
||||
|
||||
# The arguments are a delimited list
|
||||
args_list = pp.DelimitedList(single_arg)
|
||||
|
||||
# The full macro: LEDGER_ENTRY(args) or LEDGER_ENTRY_DUPLICATE(args)
|
||||
macro_parser = (
|
||||
ledger_entry + pp.Suppress("(") + pp.Group(args_list)("args") + pp.Suppress(")")
|
||||
)
|
||||
|
||||
return macro_parser
|
||||
|
||||
|
||||
def parse_ledger_entry_args(args_list):
|
||||
"""Parse the arguments of a LEDGER_ENTRY macro call.
|
||||
|
||||
Args:
|
||||
args_list: A list of parsed arguments from pyparsing, e.g.,
|
||||
['ltACCOUNT_ROOT', '0x0061', 'AccountRoot', 'account', '({...})']
|
||||
|
||||
Returns:
|
||||
A dict with parsed ledger entry information.
|
||||
"""
|
||||
if len(args_list) < 5:
|
||||
raise ValueError(
|
||||
f"Expected at least 5 parts in LEDGER_ENTRY, got {len(args_list)}: {args_list}"
|
||||
)
|
||||
|
||||
tag = args_list[0]
|
||||
value = args_list[1]
|
||||
name = args_list[2]
|
||||
rpc_name = args_list[3]
|
||||
fields_str = args_list[-1]
|
||||
|
||||
# Parse fields: ({field1, field2, ...})
|
||||
fields = parse_field_list(fields_str)
|
||||
|
||||
return {
|
||||
"tag": tag,
|
||||
"value": value,
|
||||
"name": name,
|
||||
"rpc_name": rpc_name,
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
|
||||
def parse_macro_file(file_path):
|
||||
"""Parse the ledger_entries.macro file and return a list of ledger entry definitions.
|
||||
|
||||
Uses pcpp to preprocess the file and pyparsing to parse the LEDGER_ENTRY macros.
|
||||
"""
|
||||
with open(file_path, "r") as f:
|
||||
c_code = f.read()
|
||||
|
||||
# Step 1: Clean the C++ code using pcpp
|
||||
cleaner = CppCleaner("LEDGER_ENTRY_INCLUDE", "LEDGER_ENTRY")
|
||||
cleaner.parse(c_code)
|
||||
|
||||
out = io.StringIO()
|
||||
cleaner.write(out)
|
||||
clean_text = out.getvalue()
|
||||
|
||||
# Step 2: Parse the clean text using pyparsing
|
||||
parser = create_ledger_entry_parser()
|
||||
entries = []
|
||||
|
||||
for match, _, _ in parser.scan_string(clean_text):
|
||||
# Extract the macro name and arguments
|
||||
raw_args = match.args
|
||||
|
||||
# Parse the arguments
|
||||
entry_data = parse_ledger_entry_args(raw_args)
|
||||
entries.append(entry_data)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate C++ ledger entry classes from ledger_entries.macro"
|
||||
)
|
||||
parser.add_argument("macro_path", help="Path to ledger_entries.macro")
|
||||
parser.add_argument(
|
||||
"--header-dir",
|
||||
help="Output directory for header files",
|
||||
default="include/xrpl/protocol_autogen/ledger_entries",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-dir",
|
||||
help="Output directory for test files (optional)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sfields-macro",
|
||||
help="Path to sfields.macro (default: auto-detect from macro_path)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse the macro file to get ledger entry names
|
||||
entries = parse_macro_file(args.macro_path)
|
||||
|
||||
# Auto-detect sfields.macro path if not provided
|
||||
if args.sfields_macro:
|
||||
sfields_path = Path(args.sfields_macro)
|
||||
else:
|
||||
# Assume sfields.macro is in the same directory as ledger_entries.macro
|
||||
macro_path = Path(args.macro_path)
|
||||
sfields_path = macro_path.parent / "sfields.macro"
|
||||
|
||||
# Parse sfields.macro to get field type information
|
||||
print(f"Parsing {sfields_path}...")
|
||||
field_types = parse_sfields_macro(sfields_path)
|
||||
print(
|
||||
f"Found {len(field_types)} field definitions ({sum(1 for f in field_types.values() if f['typed'])} typed, {sum(1 for f in field_types.values() if not f['typed'])} untyped)\n"
|
||||
)
|
||||
|
||||
print(f"Found {len(entries)} ledger entries\n")
|
||||
|
||||
for entry in entries:
|
||||
print(f"Ledger Entry: {entry['name']}")
|
||||
print(f" Tag: {entry['tag']}")
|
||||
print(f" Value: {entry['value']}")
|
||||
print(f" RPC Name: {entry['rpc_name']}")
|
||||
print(f" Fields: {len(entry['fields'])}")
|
||||
for field in entry["fields"]:
|
||||
mpt_info = f" ({field['mpt_support']})" if "mpt_support" in field else ""
|
||||
print(f" - {field['name']}: {field['requirement']}{mpt_info}")
|
||||
print()
|
||||
|
||||
# Set up template directory
|
||||
script_dir = Path(__file__).parent
|
||||
template_dir = script_dir / "templates"
|
||||
|
||||
# Generate C++ classes
|
||||
header_dir = Path(args.header_dir)
|
||||
header_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Clear existing generated files before regenerating
|
||||
clear_output_directory(header_dir)
|
||||
|
||||
for entry in entries:
|
||||
generate_cpp_class(
|
||||
entry, header_dir, template_dir, field_types, "LedgerEntry.h.mako"
|
||||
)
|
||||
|
||||
print(f"\nGenerated {len(entries)} ledger entry classes")
|
||||
|
||||
# Generate unit tests if --test-dir is provided
|
||||
if args.test_dir:
|
||||
test_dir = Path(args.test_dir)
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Clear existing generated test files before regenerating
|
||||
clear_output_directory(test_dir)
|
||||
|
||||
for entry in entries:
|
||||
# Fields are already enriched from generate_cpp_class above
|
||||
generate_from_template(
|
||||
entry, test_dir, template_dir, "LedgerEntryTests.cpp.mako", "Tests.cpp"
|
||||
)
|
||||
|
||||
print(f"\nGenerated {len(entries)} ledger entry test files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
230
scripts/codegen/generate_tx_classes.py
Normal file
230
scripts/codegen/generate_tx_classes.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parse transactions.macro file to extract transaction information
|
||||
and generate C++ classes for each transaction type.
|
||||
|
||||
Uses pcpp to preprocess the macro file and pyparsing to parse the DSL.
|
||||
"""
|
||||
|
||||
import io
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import pyparsing as pp
|
||||
|
||||
# Import common utilities
|
||||
from macro_parser_common import (
|
||||
CppCleaner,
|
||||
parse_sfields_macro,
|
||||
parse_field_list,
|
||||
generate_cpp_class,
|
||||
generate_from_template,
|
||||
clear_output_directory,
|
||||
)
|
||||
|
||||
|
||||
def create_transaction_parser():
|
||||
"""Create a pyparsing parser for TRANSACTION macros.
|
||||
|
||||
This parser extracts the full TRANSACTION macro call and parses its arguments
|
||||
using pyparsing's nesting-aware delimited list parsing.
|
||||
"""
|
||||
# Define nested structures so pyparsing protects them
|
||||
nested_braces = pp.original_text_for(pp.nested_expr("{", "}"))
|
||||
nested_parens = pp.original_text_for(pp.nested_expr("(", ")"))
|
||||
|
||||
# Define standard text (anything that isn't a comma, parens, or braces)
|
||||
plain_text = pp.Word(pp.printables + " \t\n", exclude_chars=",{}()")
|
||||
|
||||
# A single argument is any combination of the above
|
||||
single_arg = pp.Combine(pp.OneOrMore(nested_braces | nested_parens | plain_text))
|
||||
single_arg.set_parse_action(lambda t: t[0].strip())
|
||||
|
||||
# The arguments are a delimited list
|
||||
args_list = pp.DelimitedList(single_arg)
|
||||
|
||||
# The full macro: TRANSACTION(args)
|
||||
macro_parser = (
|
||||
pp.Keyword("TRANSACTION")
|
||||
+ pp.Suppress("(")
|
||||
+ pp.Group(args_list)("args")
|
||||
+ pp.Suppress(")")
|
||||
)
|
||||
|
||||
return macro_parser
|
||||
|
||||
|
||||
def parse_transaction_args(args_list):
|
||||
"""Parse the arguments of a TRANSACTION macro call.
|
||||
|
||||
Args:
|
||||
args_list: A list of parsed arguments from pyparsing, e.g.,
|
||||
['ttPAYMENT', '0', 'Payment', 'Delegation::delegable',
|
||||
'uint256{}', 'createAcct', '({...})']
|
||||
|
||||
Returns:
|
||||
A dict with parsed transaction information.
|
||||
"""
|
||||
if len(args_list) < 7:
|
||||
raise ValueError(
|
||||
f"Expected at least 7 parts in TRANSACTION, got {len(args_list)}: {args_list}"
|
||||
)
|
||||
|
||||
tag = args_list[0]
|
||||
value = args_list[1]
|
||||
name = args_list[2]
|
||||
delegable = args_list[3]
|
||||
amendments = args_list[4]
|
||||
privileges = args_list[5]
|
||||
fields_str = args_list[-1]
|
||||
|
||||
# Parse fields: ({field1, field2, ...})
|
||||
fields = parse_field_list(fields_str)
|
||||
|
||||
return {
|
||||
"tag": tag,
|
||||
"value": value,
|
||||
"name": name,
|
||||
"delegable": delegable,
|
||||
"amendments": amendments,
|
||||
"privileges": privileges,
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
|
||||
def parse_macro_file(filepath):
|
||||
"""Parse the transactions.macro file.
|
||||
|
||||
Uses pcpp to preprocess the file and pyparsing to parse the TRANSACTION macros.
|
||||
"""
|
||||
with open(filepath, "r") as f:
|
||||
c_code = f.read()
|
||||
|
||||
# Step 1: Clean the C++ code using pcpp
|
||||
cleaner = CppCleaner("TRANSACTION_INCLUDE", "TRANSACTION")
|
||||
cleaner.parse(c_code)
|
||||
|
||||
out = io.StringIO()
|
||||
cleaner.write(out)
|
||||
clean_text = out.getvalue()
|
||||
|
||||
# Step 2: Parse the clean text using pyparsing
|
||||
parser = create_transaction_parser()
|
||||
transactions = []
|
||||
|
||||
for match, _, _ in parser.scan_string(clean_text):
|
||||
# Extract the macro name and arguments
|
||||
raw_args = match.args
|
||||
|
||||
# Parse the arguments
|
||||
tx_data = parse_transaction_args(raw_args)
|
||||
transactions.append(tx_data)
|
||||
|
||||
return transactions
|
||||
|
||||
|
||||
# TransactionBase is a static file in the repository at:
|
||||
# - include/xrpl/protocol/TransactionBase.h
|
||||
# - src/libxrpl/protocol/TransactionBase.cpp
|
||||
# It is NOT generated by this script.
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate C++ transaction classes from transactions.macro"
|
||||
)
|
||||
parser.add_argument("macro_path", help="Path to transactions.macro")
|
||||
parser.add_argument(
|
||||
"--header-dir",
|
||||
help="Output directory for header files",
|
||||
default="include/xrpl/protocol_autogen/transactions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-dir",
|
||||
help="Output directory for test files (optional)",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sfields-macro",
|
||||
help="Path to sfields.macro (default: auto-detect from macro_path)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse the macro file to get transaction names
|
||||
transactions = parse_macro_file(args.macro_path)
|
||||
|
||||
# Auto-detect sfields.macro path if not provided
|
||||
if args.sfields_macro:
|
||||
sfields_path = Path(args.sfields_macro)
|
||||
else:
|
||||
# Assume sfields.macro is in the same directory as transactions.macro
|
||||
macro_path = Path(args.macro_path)
|
||||
sfields_path = macro_path.parent / "sfields.macro"
|
||||
|
||||
# Parse sfields.macro to get field type information
|
||||
print(f"Parsing {sfields_path}...")
|
||||
field_types = parse_sfields_macro(sfields_path)
|
||||
print(
|
||||
f"Found {len(field_types)} field definitions ({sum(1 for f in field_types.values() if f['typed'])} typed, {sum(1 for f in field_types.values() if not f['typed'])} untyped)\n"
|
||||
)
|
||||
|
||||
print(f"Found {len(transactions)} transactions\n")
|
||||
|
||||
for tx in transactions:
|
||||
print(f"Transaction: {tx['name']}")
|
||||
print(f" Tag: {tx['tag']}")
|
||||
print(f" Value: {tx['value']}")
|
||||
print(f" Fields: {len(tx['fields'])}")
|
||||
for field in tx["fields"]:
|
||||
print(f" - {field['name']}: {field['requirement']}")
|
||||
print()
|
||||
|
||||
# Set up output directory
|
||||
header_dir = Path(args.header_dir)
|
||||
header_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Clear existing generated files before regenerating
|
||||
clear_output_directory(header_dir)
|
||||
|
||||
print(f"\nGenerating header-only template classes...")
|
||||
print(f" Headers: {header_dir}\n")
|
||||
|
||||
# Set up template directory
|
||||
script_dir = Path(__file__).parent
|
||||
template_dir = script_dir / "templates"
|
||||
|
||||
generated_files = []
|
||||
for tx_info in transactions:
|
||||
header_path = generate_cpp_class(
|
||||
tx_info, header_dir, template_dir, field_types, "Transaction.h.mako"
|
||||
)
|
||||
generated_files.append(header_path)
|
||||
print(f" Generated: {tx_info['name']}.h")
|
||||
|
||||
print(
|
||||
f"\nGenerated {len(transactions)} transaction classes ({len(generated_files)} header files)"
|
||||
)
|
||||
print(f" Headers: {header_dir.absolute()}")
|
||||
|
||||
# Generate unit tests if --test-dir is provided
|
||||
if args.test_dir:
|
||||
test_dir = Path(args.test_dir)
|
||||
test_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Clear existing generated test files before regenerating
|
||||
clear_output_directory(test_dir)
|
||||
|
||||
for tx_info in transactions:
|
||||
# Fields are already enriched from generate_cpp_class above
|
||||
generate_from_template(
|
||||
tx_info,
|
||||
test_dir,
|
||||
template_dir,
|
||||
"TransactionTests.cpp.mako",
|
||||
"Tests.cpp",
|
||||
)
|
||||
|
||||
print(f"\nGenerated {len(transactions)} transaction test files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
297
scripts/codegen/macro_parser_common.py
Normal file
297
scripts/codegen/macro_parser_common.py
Normal file
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common utilities for parsing XRP Ledger macro files.
|
||||
|
||||
This module provides shared functionality for parsing transactions.macro
|
||||
and ledger_entries.macro files using pcpp and pyparsing.
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import pyparsing as pp
|
||||
from pcpp import Preprocessor
|
||||
|
||||
|
||||
def clear_output_directory(directory):
|
||||
"""Clear all generated files from an output directory.
|
||||
|
||||
Removes all .h and .cpp files from the directory, but preserves
|
||||
the directory itself and any subdirectories.
|
||||
|
||||
Args:
|
||||
directory: Path to the directory to clear
|
||||
"""
|
||||
dir_path = Path(directory)
|
||||
if not dir_path.exists():
|
||||
return
|
||||
|
||||
# Remove generated files (headers and source files)
|
||||
for pattern in ["*.h", "*.cpp"]:
|
||||
for file_path in dir_path.glob(pattern):
|
||||
file_path.unlink()
|
||||
|
||||
print(f"Cleared output directory: {dir_path}")
|
||||
|
||||
|
||||
class CppCleaner(Preprocessor):
|
||||
"""C preprocessor that removes C++ noise while preserving macro calls."""
|
||||
|
||||
def __init__(self, macro_include_name, macro_name):
|
||||
"""
|
||||
Initialize the preprocessor.
|
||||
|
||||
Args:
|
||||
macro_include_name: The name of the include flag to set to 0
|
||||
(e.g., "TRANSACTION_INCLUDE" or "LEDGER_ENTRY_INCLUDE")
|
||||
macro_name: The name of the macro to define so #if !defined() checks pass
|
||||
(e.g., "TRANSACTION" or "LEDGER_ENTRY")
|
||||
"""
|
||||
super(CppCleaner, self).__init__()
|
||||
# Define flags so #if blocks evaluate correctly
|
||||
# We set the include flag to 0 so includes are skipped
|
||||
self.define(f"{macro_include_name} 0")
|
||||
# Define the macro so #if !defined(MACRO) / #error checks pass
|
||||
# We define it to expand to itself so the macro calls remain in the output
|
||||
# for pyparsing to find and parse
|
||||
self.define(f"{macro_name}(...) {macro_name}(__VA_ARGS__)")
|
||||
# Suppress line directives
|
||||
self.line_directive = None
|
||||
|
||||
def on_error(self, file, line, msg):
|
||||
# Ignore #error directives
|
||||
pass
|
||||
|
||||
def on_include_not_found(
|
||||
self, is_malformed, is_system_include, curdir, includepath
|
||||
):
|
||||
# Ignore missing headers
|
||||
pass
|
||||
|
||||
|
||||
def parse_sfields_macro(sfields_path):
|
||||
"""
|
||||
Parse sfields.macro to determine which fields are typed vs untyped.
|
||||
|
||||
Returns a dict mapping field names to their type information:
|
||||
{
|
||||
'sfMemos': {'typed': False, 'stiSuffix': 'ARRAY', 'typeData': {...}},
|
||||
'sfAmount': {'typed': True, 'stiSuffix': 'AMOUNT', 'typeData': {...}},
|
||||
...
|
||||
}
|
||||
"""
|
||||
# Mapping from STI suffix to C++ type for untyped fields
|
||||
UNTYPED_TYPE_MAP = {
|
||||
"ARRAY": {
|
||||
"getter_method": "getFieldArray",
|
||||
"setter_method": "setFieldArray",
|
||||
"setter_use_brackets": False,
|
||||
"setter_type": "STArray const&",
|
||||
"return_type": "STArray const&",
|
||||
"return_type_optional": "std::optional<std::reference_wrapper<STArray const>>",
|
||||
},
|
||||
"OBJECT": {
|
||||
"getter_method": "getFieldObject",
|
||||
"setter_method": "setFieldObject",
|
||||
"setter_use_brackets": False,
|
||||
"setter_type": "STObject const&",
|
||||
"return_type": "STObject",
|
||||
"return_type_optional": "std::optional<STObject>",
|
||||
},
|
||||
"PATHSET": {
|
||||
"getter_method": "getFieldPathSet",
|
||||
"setter_method": "setFieldPathSet",
|
||||
"setter_use_brackets": False,
|
||||
"setter_type": "STPathSet const&",
|
||||
"return_type": "STPathSet const&",
|
||||
"return_type_optional": "std::optional<std::reference_wrapper<STPathSet const>>",
|
||||
},
|
||||
}
|
||||
|
||||
field_info = {}
|
||||
|
||||
with open(sfields_path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse TYPED_SFIELD entries
|
||||
# Format: TYPED_SFIELD(sfName, stiSuffix, fieldValue, ...)
|
||||
typed_pattern = r"TYPED_SFIELD\s*\(\s*(\w+)\s*,\s*(\w+)\s*,"
|
||||
for match in re.finditer(typed_pattern, content):
|
||||
field_name = match.group(1)
|
||||
sti_suffix = match.group(2)
|
||||
field_info[field_name] = {
|
||||
"typed": True,
|
||||
"stiSuffix": sti_suffix,
|
||||
"typeData": {
|
||||
"getter_method": "at",
|
||||
"setter_method": "",
|
||||
"setter_use_brackets": True,
|
||||
"setter_type": f"std::decay_t<typename SF_{sti_suffix}::type::value_type> const&",
|
||||
"return_type": f"SF_{sti_suffix}::type::value_type",
|
||||
"return_type_optional": f"protocol_autogen::Optional<SF_{sti_suffix}::type::value_type>",
|
||||
},
|
||||
}
|
||||
|
||||
# Parse UNTYPED_SFIELD entries
|
||||
# Format: UNTYPED_SFIELD(sfName, stiSuffix, fieldValue, ...)
|
||||
untyped_pattern = r"UNTYPED_SFIELD\s*\(\s*(\w+)\s*,\s*(\w+)\s*,"
|
||||
for match in re.finditer(untyped_pattern, content):
|
||||
field_name = match.group(1)
|
||||
sti_suffix = match.group(2)
|
||||
type_data = UNTYPED_TYPE_MAP.get(
|
||||
sti_suffix, UNTYPED_TYPE_MAP.get("OBJECT")
|
||||
) # Default to OBJECT
|
||||
field_info[field_name] = {
|
||||
"typed": False,
|
||||
"stiSuffix": sti_suffix,
|
||||
"typeData": type_data,
|
||||
}
|
||||
|
||||
return field_info
|
||||
|
||||
|
||||
def create_field_list_parser():
|
||||
"""Create a pyparsing parser for field lists like '({...})'."""
|
||||
# A field identifier (e.g., sfDestination, soeREQUIRED, soeMPTSupported)
|
||||
field_identifier = pp.Word(pp.alphas + "_", pp.alphanums + "_")
|
||||
|
||||
# A single field definition: {sfName, soeREQUIRED, ...}
|
||||
# Allow optional trailing comma inside the braces
|
||||
field_def = (
|
||||
pp.Suppress("{")
|
||||
+ pp.Group(pp.DelimitedList(field_identifier) + pp.Optional(pp.Suppress(",")))(
|
||||
"parts"
|
||||
)
|
||||
+ pp.Suppress("}")
|
||||
)
|
||||
|
||||
# The field list: ({field1, field2, ...}) or ({}) for empty lists
|
||||
# Allow optional trailing comma after the last field definition
|
||||
field_list = (
|
||||
pp.Suppress("(")
|
||||
+ pp.Suppress("{")
|
||||
+ pp.Group(
|
||||
pp.Optional(pp.DelimitedList(field_def) + pp.Optional(pp.Suppress(",")))
|
||||
)("fields")
|
||||
+ pp.Suppress("}")
|
||||
+ pp.Suppress(")")
|
||||
)
|
||||
|
||||
return field_list
|
||||
|
||||
|
||||
def parse_field_list(fields_str):
|
||||
"""Parse a field list string like '({...})' using pyparsing.
|
||||
|
||||
Args:
|
||||
fields_str: A string like '({
|
||||
{sfDestination, soeREQUIRED},
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported}
|
||||
})'
|
||||
|
||||
Returns:
|
||||
A list of field dicts with 'name', 'requirement', 'flags', and 'supports_mpt'.
|
||||
"""
|
||||
parser = create_field_list_parser()
|
||||
|
||||
try:
|
||||
result = parser.parse_string(fields_str, parse_all=True)
|
||||
fields = []
|
||||
|
||||
for field_parts in result.fields:
|
||||
if len(field_parts) < 2:
|
||||
continue
|
||||
|
||||
field_name = field_parts[0]
|
||||
requirement = field_parts[1]
|
||||
flags = list(field_parts[2:]) if len(field_parts) > 2 else []
|
||||
supports_mpt = "soeMPTSupported" in flags
|
||||
|
||||
fields.append(
|
||||
{
|
||||
"name": field_name,
|
||||
"requirement": requirement,
|
||||
"flags": flags,
|
||||
"supports_mpt": supports_mpt,
|
||||
}
|
||||
)
|
||||
|
||||
return fields
|
||||
except pp.ParseException as e:
|
||||
raise ValueError(f"Failed to parse field list: {e}")
|
||||
|
||||
|
||||
def enrich_fields_with_type_data(entry_info, field_types):
|
||||
"""Enrich field information with type data from sfields.macro.
|
||||
|
||||
Args:
|
||||
entry_info: Dict containing entry information (name, fields, etc.)
|
||||
field_types: Dict mapping field names to type information
|
||||
|
||||
Modifies entry_info["fields"] in place.
|
||||
"""
|
||||
for field in entry_info["fields"]:
|
||||
field_name = field["name"]
|
||||
if field_name in field_types:
|
||||
field["typed"] = field_types[field_name]["typed"]
|
||||
field["paramName"] = field_name[2].lower() + field_name[3:]
|
||||
field["stiSuffix"] = field_types[field_name]["stiSuffix"]
|
||||
field["typeData"] = field_types[field_name]["typeData"]
|
||||
else:
|
||||
# Unknown field - assume typed for safety
|
||||
field["typed"] = True
|
||||
field["paramName"] = ""
|
||||
field["stiSuffix"] = None
|
||||
field["typeData"] = None
|
||||
|
||||
|
||||
def generate_from_template(
|
||||
entry_info, output_dir, template_dir, template_name, output_suffix
|
||||
):
|
||||
"""Generate a file from a Mako template.
|
||||
|
||||
Args:
|
||||
entry_info: Dict containing entry information (name, fields, etc.)
|
||||
Fields should already be enriched with type data.
|
||||
output_dir: Output directory for generated files
|
||||
template_dir: Directory containing Mako templates
|
||||
template_name: Name of the Mako template file to use
|
||||
output_suffix: Suffix for the output file (e.g., ".h" or "Tests.cpp")
|
||||
|
||||
Returns:
|
||||
Path to the generated file
|
||||
"""
|
||||
from mako.template import Template
|
||||
|
||||
template_path = Path(template_dir) / template_name
|
||||
template = Template(filename=str(template_path))
|
||||
|
||||
# Render the template - pass entry_info directly so templates can access any field
|
||||
content = template.render(**entry_info)
|
||||
|
||||
# Write output file in binary mode to avoid any line ending conversion
|
||||
output_path = Path(output_dir) / f"{entry_info['name']}{output_suffix}"
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(content.encode("utf-8"))
|
||||
|
||||
print(f"Generated {output_path}")
|
||||
return output_path
|
||||
|
||||
|
||||
def generate_cpp_class(
|
||||
entry_info, header_dir, template_dir, field_types, template_name
|
||||
):
|
||||
"""Generate C++ header file from a Mako template.
|
||||
|
||||
Args:
|
||||
entry_info: Dict containing entry information (name, fields, etc.)
|
||||
header_dir: Output directory for generated header files
|
||||
template_dir: Directory containing Mako templates
|
||||
field_types: Dict mapping field names to type information
|
||||
template_name: Name of the Mako template file to use
|
||||
"""
|
||||
# Enrich field information with type data
|
||||
enrich_fields_with_type_data(entry_info, field_types)
|
||||
|
||||
# Generate the header file
|
||||
generate_from_template(entry_info, header_dir, template_dir, template_name, ".h")
|
||||
13
scripts/codegen/requirements.txt
Normal file
13
scripts/codegen/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Python dependencies for XRP Ledger code generation scripts
|
||||
#
|
||||
# These packages are required to run the code generation scripts that
|
||||
# parse macro files and generate C++ wrapper classes.
|
||||
|
||||
# C preprocessor for Python - used to preprocess macro files
|
||||
pcpp>=1.30
|
||||
|
||||
# Parser combinator library - used to parse the macro DSL
|
||||
pyparsing>=3.0.0
|
||||
|
||||
# Template engine - used to generate C++ code from templates
|
||||
Mako>=1.2.0
|
||||
216
scripts/codegen/templates/LedgerEntry.h.mako
Normal file
216
scripts/codegen/templates/LedgerEntry.h.mako
Normal file
@@ -0,0 +1,216 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
class ${name}Builder;
|
||||
|
||||
/**
|
||||
* @brief Ledger Entry: ${name}
|
||||
*
|
||||
* Type: ${tag} (${value})
|
||||
* RPC Name: ${rpc_name}
|
||||
*
|
||||
* Immutable wrapper around SLE providing type-safe field access.
|
||||
* Use ${name}Builder to construct new ledger entries.
|
||||
*/
|
||||
class ${name} : public LedgerEntryBase
|
||||
{
|
||||
public:
|
||||
static constexpr LedgerEntryType entryType = ${tag};
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name} ledger entry wrapper from an existing SLE object.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
explicit ${name}(std::shared_ptr<SLE const> sle)
|
||||
: LedgerEntryBase(std::move(sle))
|
||||
{
|
||||
// Verify ledger entry type
|
||||
if (sle_->getType() != entryType)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for ${name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Ledger entry-specific field getters
|
||||
% for field in fields:
|
||||
% if field['typed']:
|
||||
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('mpt_support'):
|
||||
* MPT Support: ${field['mpt_support']}
|
||||
% endif
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (has${field['name'][2:]}())
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% else:
|
||||
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('mpt_support'):
|
||||
* MPT Support: ${field['mpt_support']}
|
||||
% endif
|
||||
* @note This is an untyped field (${field.get('cppType', 'unknown')}).
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (this->sle_->isFieldPresent(${field['name']}))
|
||||
return this->sle_->${field['typeData']['getter_method']}(${field['name']});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
};
|
||||
|
||||
<%
|
||||
required_fields = [f for f in fields if f['requirement'] == 'soeREQUIRED']
|
||||
%>\
|
||||
/**
|
||||
* @brief Builder for ${name} ledger entries.
|
||||
*
|
||||
* Provides a fluent interface for constructing ledger entries with method chaining.
|
||||
* Uses Json::Value internally for flexible ledger entry construction.
|
||||
* Inherits common field setters from LedgerEntryBuilderBase.
|
||||
*/
|
||||
class ${name}Builder : public LedgerEntryBuilderBase<${name}Builder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ${name}Builder with required fields.
|
||||
% for field in required_fields:
|
||||
* @param ${field['paramName']} The ${field['name']} field value.
|
||||
% endfor
|
||||
*/
|
||||
${name}Builder(\
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field['typeData']['setter_type']} ${field['paramName']}${',' if i < len(required_fields) - 1 else ''}\
|
||||
% endfor
|
||||
)
|
||||
: LedgerEntryBuilderBase<${name}Builder>(${tag})
|
||||
{
|
||||
% for field in required_fields:
|
||||
set${field['name'][2:]}(${field['paramName']});
|
||||
% endfor
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name}Builder from an existing SLE object.
|
||||
* @param sle The existing ledger entry to copy from.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
${name}Builder(std::shared_ptr<SLE const> sle)
|
||||
{
|
||||
if (sle->at(sfLedgerEntryType) != ${tag})
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for ${name}");
|
||||
}
|
||||
object_ = *sle;
|
||||
}
|
||||
|
||||
/** @brief Ledger entry-specific field setters */
|
||||
% for field in fields:
|
||||
|
||||
/**
|
||||
* @brief Set ${field['name']} (${field['requirement']})
|
||||
% if field.get('mpt_support'):
|
||||
* MPT Support: ${field['mpt_support']}
|
||||
% endif
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
${name}Builder&
|
||||
set${field['name'][2:]}(${field['typeData']['setter_type']} value)
|
||||
{
|
||||
% if field.get('stiSuffix') == 'ISSUE':
|
||||
object_[${field['name']}] = STIssue(${field['name']}, value);
|
||||
% elif field['typeData'].get('setter_use_brackets'):
|
||||
object_[${field['name']}] = value;
|
||||
% else:
|
||||
object_.${field['typeData']['setter_method']}(${field['name']}, value);
|
||||
% endif
|
||||
return *this;
|
||||
}
|
||||
% endfor
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed ${name} wrapper.
|
||||
* @param index The ledger entry index.
|
||||
* @return The constructed ledger entry wrapper.
|
||||
*/
|
||||
${name}
|
||||
build(uint256 const& index)
|
||||
{
|
||||
return ${name}{std::make_shared<SLE>(std::move(object_), index)};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
231
scripts/codegen/templates/LedgerEntryTests.cpp.mako
Normal file
231
scripts/codegen/templates/LedgerEntryTests.cpp.mako
Normal file
@@ -0,0 +1,231 @@
|
||||
// Auto-generated unit tests for ledger entry ${name}
|
||||
<%
|
||||
required_fields = [f for f in fields if f["requirement"] == "soeREQUIRED"]
|
||||
optional_fields = [f for f in fields if f["requirement"] != "soeREQUIRED"]
|
||||
|
||||
def canonical_expr(field):
|
||||
return f"canonical_{field['stiSuffix']}()"
|
||||
|
||||
# Pick a wrong ledger entry to test type mismatch
|
||||
# Use Ticket as it has minimal required fields (just Account)
|
||||
if name != "Ticket":
|
||||
wrong_le_include = "Ticket"
|
||||
else:
|
||||
wrong_le_include = "Check"
|
||||
%>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/${name}.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/${wrong_le_include}.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed for both the
|
||||
// builder's STObject and the wrapper's SLE.
|
||||
TEST(${name}Tests, BuilderSettersRoundTrip)
|
||||
{
|
||||
uint256 const index{1u};
|
||||
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field["paramName"]}Value${"," if i < len(required_fields) - 1 else ""}
|
||||
% endfor
|
||||
};
|
||||
|
||||
% for field in optional_fields:
|
||||
builder.set${field["name"][2:]}(${field["paramName"]}Value);
|
||||
% endfor
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
|
||||
EXPECT_TRUE(builder.validate());
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
EXPECT_TRUE(entry.validate());
|
||||
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actual = entry.get${field["name"][2:]}();
|
||||
expectEqualField(expected, actual, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actualOpt = entry.get${field["name"][2:]}();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "${field["name"]}");
|
||||
EXPECT_TRUE(entry.has${field["name"][2:]}());
|
||||
}
|
||||
|
||||
% endfor
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
EXPECT_EQ(*ledgerIndex, index);
|
||||
EXPECT_EQ(entry.getKey(), index);
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
|
||||
// from that SLE, build a new wrapper, and verify all fields (and validate()).
|
||||
TEST(${name}Tests, BuilderFromSleRoundTrip)
|
||||
{
|
||||
uint256 const index{2u};
|
||||
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
auto sle = std::make_shared<SLE>(${name}::entryType, index);
|
||||
|
||||
% for field in fields:
|
||||
% if field.get("stiSuffix") == "ISSUE":
|
||||
sle->at(${field["name"]}) = STIssue(${field["name"]}, ${field["paramName"]}Value);
|
||||
% elif field["typeData"].get("setter_use_brackets"):
|
||||
sle->at(${field["name"]}) = ${field["paramName"]}Value;
|
||||
% else:
|
||||
sle->${field["typeData"]["setter_method"]}(${field["name"]}, ${field["paramName"]}Value);
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
${name}Builder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
|
||||
auto const entryFromBuilder = builderFromSle.build(index);
|
||||
|
||||
${name} entryFromSle{sle};
|
||||
EXPECT_TRUE(entryFromBuilder.validate());
|
||||
EXPECT_TRUE(entryFromSle.validate());
|
||||
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
|
||||
auto const fromSle = entryFromSle.get${field["name"][2:]}();
|
||||
auto const fromBuilder = entryFromBuilder.get${field["name"][2:]}();
|
||||
|
||||
expectEqualField(expected, fromSle, "${field["name"]}");
|
||||
expectEqualField(expected, fromBuilder, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.get${field["name"][2:]}();
|
||||
auto const fromBuilderOpt = entryFromBuilder.get${field["name"][2:]}();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "${field["name"]}");
|
||||
expectEqualField(expected, *fromBuilderOpt, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
|
||||
TEST(${name}Tests, WrapperThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
|
||||
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
|
||||
% if wrong_le_include == "Ticket":
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% else:
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_ACCOUNT(),
|
||||
canonical_AMOUNT(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% endif
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(${name}{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong ledger entry type.
|
||||
TEST(${name}Tests, BuilderThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{4u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
% if wrong_le_include == "Ticket":
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% else:
|
||||
${wrong_le_include}Builder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_ACCOUNT(),
|
||||
canonical_AMOUNT(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
% endif
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(${name}Builder{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
% if optional_fields:
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(${name}Tests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
% for field in required_fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field["paramName"]}Value${"," if i < len(required_fields) - 1 else ""}
|
||||
% endfor
|
||||
};
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
// Verify optional fields are not present
|
||||
% for field in optional_fields:
|
||||
EXPECT_FALSE(entry.has${field["name"][2:]}());
|
||||
EXPECT_FALSE(entry.get${field["name"][2:]}().has_value());
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
}
|
||||
226
scripts/codegen/templates/Transaction.h.mako
Normal file
226
scripts/codegen/templates/Transaction.h.mako
Normal file
@@ -0,0 +1,226 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class ${name}Builder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: ${name}
|
||||
*
|
||||
* Type: ${tag} (${value})
|
||||
* Delegable: ${delegable}
|
||||
* Amendment: ${amendments}
|
||||
* Privileges: ${privileges}
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use ${name}Builder to construct new transactions.
|
||||
*/
|
||||
class ${name} : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ${tag};
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name} transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit ${name}(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ${name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
% for field in fields:
|
||||
% if field['typed']:
|
||||
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('supports_mpt'):
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
% endif
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (has${field['name'][2:]}())
|
||||
{
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% else:
|
||||
/**
|
||||
* @brief Get ${field['name']} (${field['requirement']})
|
||||
% if field.get('supports_mpt'):
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
% endif
|
||||
* @note This is an untyped field.
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
* @return The field value.
|
||||
% else:
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
% endif
|
||||
*/
|
||||
% if field['requirement'] == 'soeREQUIRED':
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
}
|
||||
% else:
|
||||
[[nodiscard]]
|
||||
${field['typeData']['return_type_optional']}
|
||||
get${field['name'][2:]}() const
|
||||
{
|
||||
if (this->tx_->isFieldPresent(${field['name']}))
|
||||
return this->tx_->${field['typeData']['getter_method']}(${field['name']});
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if ${field['name']} is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
has${field['name'][2:]}() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(${field['name']});
|
||||
}
|
||||
% endif
|
||||
% endif
|
||||
% endfor
|
||||
};
|
||||
|
||||
<%
|
||||
required_fields = [f for f in fields if f['requirement'] == 'soeREQUIRED']
|
||||
%>\
|
||||
/**
|
||||
* @brief Builder for ${name} transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses Json::Value internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class ${name}Builder : public TransactionBuilderBase<${name}Builder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ${name}Builder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
% for field in required_fields:
|
||||
* @param ${field['paramName']} The ${field['name']} field value.
|
||||
% endfor
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
${name}Builder(SF_ACCOUNT::type::value_type account,
|
||||
% for i, field in enumerate(required_fields):
|
||||
${field['typeData']['setter_type']} ${field['paramName']},\
|
||||
% endfor
|
||||
std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<${name}Builder>(${tag}, account, sequence, fee)
|
||||
{
|
||||
% for field in required_fields:
|
||||
set${field['name'][2:]}(${field['paramName']});
|
||||
% endfor
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a ${name}Builder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
${name}Builder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ${tag})
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for ${name}Builder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/** @brief Transaction-specific field setters */
|
||||
% for field in fields:
|
||||
|
||||
/**
|
||||
* @brief Set ${field['name']} (${field['requirement']})
|
||||
% if field.get('supports_mpt'):
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
% endif
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
${name}Builder&
|
||||
set${field['name'][2:]}(${field['typeData']['setter_type']} value)
|
||||
{
|
||||
% if field.get('stiSuffix') == 'ISSUE':
|
||||
object_[${field['name']}] = STIssue(${field['name']}, value);
|
||||
% elif field['typeData'].get('setter_use_brackets'):
|
||||
object_[${field['name']}] = value;
|
||||
% else:
|
||||
object_.${field['typeData']['setter_method']}(${field['name']}, value);
|
||||
% endif
|
||||
return *this;
|
||||
}
|
||||
% endfor
|
||||
|
||||
/**
|
||||
* @brief Build and return the ${name} wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
${name}
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return ${name}{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
241
scripts/codegen/templates/TransactionTests.cpp.mako
Normal file
241
scripts/codegen/templates/TransactionTests.cpp.mako
Normal file
@@ -0,0 +1,241 @@
|
||||
// Auto-generated unit tests for transaction ${name}
|
||||
<%
|
||||
required_fields = [f for f in fields if f["requirement"] == "soeREQUIRED"]
|
||||
optional_fields = [f for f in fields if f["requirement"] != "soeREQUIRED"]
|
||||
|
||||
def canonical_expr(field):
|
||||
return f"canonical_{field['stiSuffix']}()"
|
||||
|
||||
# Pick a wrong transaction to test type mismatch
|
||||
if name != "AccountSet":
|
||||
wrong_tx_include = "AccountSet"
|
||||
else:
|
||||
wrong_tx_include = "OfferCancel"
|
||||
%>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol_autogen/transactions/${name}.h>
|
||||
#include <xrpl/protocol_autogen/transactions/${wrong_tx_include}.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed.
|
||||
TEST(Transactions${name}Tests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("test${name}"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
accountValue,
|
||||
% for field in required_fields:
|
||||
${field["paramName"]}Value,
|
||||
% endfor
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
% for field in optional_fields:
|
||||
builder.set${field["name"][2:]}(${field["paramName"]}Value);
|
||||
% endfor
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actual = tx.get${field["name"][2:]}();
|
||||
expectEqualField(expected, actual, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
// Verify optional fields
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actualOpt = tx.get${field["name"][2:]}();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field ${field["name"]} should be present";
|
||||
expectEqualField(expected, *actualOpt, "${field["name"]}");
|
||||
EXPECT_TRUE(tx.has${field["name"][2:]}());
|
||||
}
|
||||
|
||||
% endfor
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(Transactions${name}Tests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("test${name}FromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
% for field in fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
// Build an initial transaction
|
||||
${name}Builder initialBuilder{
|
||||
accountValue,
|
||||
% for field in required_fields:
|
||||
${field["paramName"]}Value,
|
||||
% endfor
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
% for field in optional_fields:
|
||||
initialBuilder.set${field["name"][2:]}(${field["paramName"]}Value);
|
||||
% endfor
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
${name}Builder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
% for field in required_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actual = rebuiltTx.get${field["name"][2:]}();
|
||||
expectEqualField(expected, actual, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
// Verify optional fields
|
||||
% for field in optional_fields:
|
||||
{
|
||||
auto const& expected = ${field["paramName"]}Value;
|
||||
auto const actualOpt = rebuiltTx.get${field["name"][2:]}();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field ${field["name"]} should be present";
|
||||
expectEqualField(expected, *actualOpt, "${field["name"]}");
|
||||
}
|
||||
|
||||
% endfor
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(Transactions${name}Tests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
% if wrong_tx_include == "AccountSet":
|
||||
${wrong_tx_include}Builder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
% else:
|
||||
${wrong_tx_include}Builder wrongBuilder{account, canonical_UINT32(), 1, canonical_AMOUNT()};
|
||||
% endif
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(${name}{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(Transactions${name}Tests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
% if wrong_tx_include == "AccountSet":
|
||||
${wrong_tx_include}Builder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
% else:
|
||||
${wrong_tx_include}Builder wrongBuilder{account, canonical_UINT32(), 1, canonical_AMOUNT()};
|
||||
% endif
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(${name}Builder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
% if optional_fields:
|
||||
// 5) Build with only required fields and verify optional fields return nullopt.
|
||||
TEST(Transactions${name}Tests, OptionalFieldsReturnNullopt)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::secp256k1, generateSeed("test${name}Nullopt"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 3;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific required field values
|
||||
% for field in required_fields:
|
||||
auto const ${field["paramName"]}Value = ${canonical_expr(field)};
|
||||
% endfor
|
||||
|
||||
${name}Builder builder{
|
||||
accountValue,
|
||||
% for field in required_fields:
|
||||
${field["paramName"]}Value,
|
||||
% endfor
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Do NOT set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
% for field in optional_fields:
|
||||
EXPECT_FALSE(tx.has${field["name"][2:]}());
|
||||
EXPECT_FALSE(tx.get${field["name"][2:]}().has_value());
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
|
||||
}
|
||||
80
scripts/codegen/update_codegen_stamp.py
Normal file
80
scripts/codegen/update_codegen_stamp.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check or update the codegen stamp file.
|
||||
|
||||
Uses only the Python standard library (hashlib, pathlib, sys) so it can
|
||||
run without a virtual environment.
|
||||
|
||||
Modes:
|
||||
--check Exit 0 if stamp is up-to-date, exit 1 if stale/missing.
|
||||
--update Recompute the hash and write it to the stamp file.
|
||||
|
||||
Usage:
|
||||
python update_codegen_stamp.py --check <stamp_file> <input_files...>
|
||||
python update_codegen_stamp.py --update <stamp_file> <input_files...>
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def compute_combined_hash(input_files: list[str]) -> str:
|
||||
"""Compute a combined SHA-256 hash of all input files.
|
||||
|
||||
Algorithm: compute each file's SHA-256 hex digest, concatenate them
|
||||
all, then SHA-256 the concatenation.
|
||||
"""
|
||||
parts = []
|
||||
for filepath in input_files:
|
||||
file_hash = hashlib.sha256(Path(filepath).read_bytes()).hexdigest()
|
||||
parts.append(file_hash)
|
||||
|
||||
combined = "".join(parts)
|
||||
return hashlib.sha256(combined.encode()).hexdigest()
|
||||
|
||||
|
||||
def read_stamp_hash(stamp_file: str) -> str:
|
||||
"""Read the COMBINED_HASH from an existing stamp file, or '' if missing."""
|
||||
path = Path(stamp_file)
|
||||
if not path.exists():
|
||||
return ""
|
||||
for line in path.read_text().splitlines():
|
||||
if line.startswith("COMBINED_HASH="):
|
||||
return line.split("=", 1)[1]
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4 or sys.argv[1] not in ("--check", "--update"):
|
||||
print(
|
||||
f"Usage: {sys.argv[0]} --check|--update <stamp_file> <input_files...>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
mode = sys.argv[1]
|
||||
stamp_file = sys.argv[2]
|
||||
input_files = sys.argv[3:]
|
||||
|
||||
current_hash = compute_combined_hash(input_files)
|
||||
|
||||
if mode == "--check":
|
||||
stamp_hash = read_stamp_hash(stamp_file)
|
||||
if current_hash == stamp_hash:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
# --update
|
||||
with open(stamp_file, "w") as fp:
|
||||
fp.write(
|
||||
"# Auto-generated by protocol autogen - do not edit manually.\n"
|
||||
"# This file tracks input hashes to avoid unnecessary code regeneration.\n"
|
||||
"# It should be checked into version control alongside the generated files.\n"
|
||||
)
|
||||
fp.write(f"COMBINED_HASH={current_hash}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user