Refactor the code generation process

This commit is contained in:
JCW
2026-03-31 16:22:50 +01:00
parent 5c8dfe5456
commit a1344b91c3
14 changed files with 193 additions and 173 deletions

View 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

View 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()

View 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()

View 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")

View 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

View 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

View 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
}

View 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

View 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
}

View 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()