Skip to content

Commit a2b1508

Browse files
committed
add custom section mappings and template support
1 parent fe86039 commit a2b1508

File tree

8 files changed

+276
-121
lines changed

8 files changed

+276
-121
lines changed

contrib/tools/config-docs-generator/Dockerfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ WORKDIR /project_root
1616

1717
# Set environment variables for generate-config-docs.sh
1818
ENV PROJECT_ROOT=/project_root
19-
ENV BUILD_ROOT=/build
2019
ENV CARGO_HOME=/project_root/.cargo
20+
ENV CARGO_TARGET_DIR=/tmp/stacks-config-docs/target
21+
ENV TEMP_DIR=/tmp/stacks-config-docs/doc-generation
2122
ENV EXTRACT_DOCS_BIN=/build/target/release/extract-docs
2223
ENV GENERATE_MARKDOWN_BIN=/build/target/release/generate-markdown
2324
ENV SKIP_BUILD=true
2425

25-
# Set the entrypoint to run the config docs generation script
26-
# The script ends up at /build/generate-config-docs.sh due to the copy operation
26+
# Create the Docker-specific temp directory
27+
RUN mkdir -p /tmp/stacks-config-docs
28+
2729
ENTRYPOINT ["/build/generate-config-docs.sh"]

contrib/tools/config-docs-generator/README.md

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ This tool automatically generates markdown documentation from Rust configuration
99
The easiest way to generate configuration documentation:
1010

1111
```bash
12+
# Navigate to the config-docs-generator directory
13+
cd contrib/tools/config-docs-generator
14+
1215
# Build the Docker image (one-time setup)
1316
docker build -t config-docs-generator .
1417

@@ -28,8 +31,11 @@ If you prefer to run without Docker:
2831
# Install nightly toolchain if needed
2932
rustup toolchain install nightly
3033

34+
# Navigate to the config-docs-generator directory
35+
cd contrib/tools/config-docs-generator
36+
3137
# Generate documentation
32-
./contrib/tools/config-docs-generator/generate-config-docs.sh
38+
./generate-config-docs.sh
3339
```
3440

3541
## What It Does
@@ -40,8 +46,8 @@ The tool processes these configuration structs from the Stacks codebase:
4046
- `MinerConfig``[miner]` section
4147
- `ConnectionOptionsFile``[connection_options]` section
4248
- `FeeEstimationConfigFile``[fee_estimation]` section
43-
- `EventObserverConfigFile``[event_observer]` section
44-
- `InitialBalanceFile``[initial_balances]` section
49+
- `EventObserverConfigFile``[[events_observer]]` section
50+
- `InitialBalanceFile``[[ustx_balance]]` section
4551

4652
For each configuration field, it extracts:
4753
- Field documentation from `///` comments
@@ -335,31 +341,20 @@ pub struct YourNewConfig {
335341
- **@deprecated**: Deprecation message
336342
- **@toml_example**: Example TOML configuration
337343

338-
### 3. Add Section Mapping (Optional)
344+
### 3. Generate
339345

340-
If you want a custom TOML section name, edit `src/generate_markdown.rs`:
341-
342-
```rust
343-
fn struct_to_section_name(struct_name: &str) -> String {
344-
match struct_name {
345-
"YourNewConfig" => "[your_custom_section]".to_string(),
346-
// ... existing mappings
347-
_ => format!("[{}]", struct_name.to_lowercase()),
348-
}
349-
}
350-
```
351-
352-
### 4. Generate and Verify
346+
Override TOML section names using JSON configuration:
353347

354348
```bash
355-
# Using Docker (recommended)
356-
docker run --rm -v "$(pwd)/../../../:/project_root" config-docs-generator
349+
# Using Docker with custom mappings and template
350+
cd contrib/tools/config-docs-generator
351+
docker run --rm -v "$(pwd)/../../../:/project_root" \
352+
-e SECTION_MAPPINGS_PATH="/build/contrib/tools/config-docs-generator/custom_mappings.json" \
353+
-e TEMPLATE_PATH="/build/contrib/tools/config-docs-generator/templates/custom_template.md" \
354+
config-docs-generator
357355

358356
# OR using local setup
359-
./contrib/tools/config-docs-generator/generate-config-docs.sh
360-
361-
# Check that your struct appears
362-
grep -A 5 "your_custom_section" docs/generated/configuration-reference.md
357+
./generate-config-docs.sh --section-name-mappings custom_mappings.json --template custom_template.md
363358
```
364359

365360
## How It Works

contrib/tools/config-docs-generator/generate-config-docs.sh

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@ NC='\033[0m' # No Color
1111
# Configuration - Allow environment variable overrides
1212
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1313
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../../../" && pwd)}"
14-
BUILD_ROOT="${BUILD_ROOT:-$PROJECT_ROOT}"
1514
OUTPUT_DIR="$PROJECT_ROOT/docs/generated"
16-
TEMP_DIR="$PROJECT_ROOT/target/doc-generation"
17-
CONFIG_SOURCE_FILE="$PROJECT_ROOT/stackslib/src/config/mod.rs"
15+
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PROJECT_ROOT/target}"
16+
TEMP_DIR="${TEMP_DIR:-$CARGO_TARGET_DIR/doc-generation}"
1817

19-
# Paths to binaries - allow override via environment
20-
EXTRACT_DOCS_BIN="${EXTRACT_DOCS_BIN:-$BUILD_ROOT/target/release/extract-docs}"
21-
GENERATE_MARKDOWN_BIN="${GENERATE_MARKDOWN_BIN:-$BUILD_ROOT/target/release/generate-markdown}"
18+
# Binary paths - allow override via environment
19+
EXTRACT_DOCS_BIN="${EXTRACT_DOCS_BIN:-$CARGO_TARGET_DIR/release/extract-docs}"
20+
GENERATE_MARKDOWN_BIN="${GENERATE_MARKDOWN_BIN:-$CARGO_TARGET_DIR/release/generate-markdown}"
21+
22+
# Template and mappings paths - allow override via environment
23+
TEMPLATE_PATH="${TEMPLATE_PATH:-$SCRIPT_DIR/templates/reference_template.md}"
24+
SECTION_MAPPINGS_PATH="${SECTION_MAPPINGS_PATH:-$SCRIPT_DIR/section_name_mappings.json}"
2225

2326
# Check if binaries are pre-built (skip build step)
2427
SKIP_BUILD="${SKIP_BUILD:-false}"
25-
if [[ -f "$EXTRACT_DOCS_BIN" && -f "$GENERATE_MARKDOWN_BIN" ]]; then
26-
SKIP_BUILD=true
27-
fi
28+
29+
export CARGO_TARGET_DIR
2830

2931
log_info() {
3032
echo -e "${GREEN}[INFO]${NC} $1"
@@ -55,16 +57,8 @@ main() {
5557

5658
cd "$PROJECT_ROOT"
5759

58-
# Verify source file exists
59-
if [[ ! -f "$CONFIG_SOURCE_FILE" ]]; then
60-
log_error "Config source file not found: $CONFIG_SOURCE_FILE"
61-
exit 1
62-
fi
63-
64-
# Step 1: Build the documentation generation tools (skip if pre-built)
65-
if [[ "$SKIP_BUILD" == "true" ]]; then
66-
log_info "Using pre-built documentation generation tools..."
67-
else
60+
# Step 1: Build the documentation generation tools
61+
if [[ "$SKIP_BUILD" != "true" ]]; then
6862
log_info "Building documentation generation tools..."
6963
cargo build --package config-docs-generator --release
7064
fi
@@ -84,9 +78,9 @@ main() {
8478
# Step 3: Generate Markdown
8579
log_info "Generating Markdown documentation..."
8680
MARKDOWN_OUTPUT="$OUTPUT_DIR/configuration-reference.md"
87-
"$GENERATE_MARKDOWN_BIN" \
88-
--input "$EXTRACTED_JSON" \
89-
--output "$MARKDOWN_OUTPUT"
81+
82+
# Call the command
83+
"$GENERATE_MARKDOWN_BIN" --input "$EXTRACTED_JSON" --output "$MARKDOWN_OUTPUT" --template "$TEMPLATE_PATH" --section-name-mappings "$SECTION_MAPPINGS_PATH"
9084

9185
log_info "Documentation generation complete!"
9286
log_info "Generated files:"
@@ -145,6 +139,7 @@ while [[ $# -gt 0 ]]; do
145139
exit 1
146140
;;
147141
esac
142+
shift
148143
done
149144

150145
main "$@"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"BurnchainConfig": "[burnchain]",
3+
"NodeConfig": "[node]",
4+
"MinerConfig": "[miner]",
5+
"ConnectionOptionsFile": "[connection_options]",
6+
"FeeEstimationConfigFile": "[fee_estimation]",
7+
"EventObserverConfigFile": "[[events_observer]]",
8+
"InitialBalanceFile": "[[ustx_balance]]"
9+
}

contrib/tools/config-docs-generator/src/extract_docs.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn main() -> Result<()> {
7575
.long("structs")
7676
.value_name("NAMES")
7777
.help("Comma-separated list of struct names to extract")
78-
.required(false),
78+
.required(true),
7979
)
8080
.get_matches();
8181

@@ -109,6 +109,11 @@ fn generate_rustdoc_json(package: &str) -> Result<serde_json::Value> {
109109
// constants referenced in doc comments are added to the project
110110
let additional_crates = ["stacks-common"];
111111

112+
// Respect CARGO_TARGET_DIR environment variable for rustdoc output
113+
let rustdoc_target_dir = std::env::var("CARGO_TARGET_DIR")
114+
.unwrap_or_else(|_| "target".to_string())
115+
+ "/rustdoc-json";
116+
112117
// WARNING: This tool relies on nightly rustdoc JSON output (-Z unstable-options --output-format json)
113118
// The JSON format is subject to change with new Rust nightly versions and could break this tool.
114119
// Use cargo rustdoc with nightly to generate JSON for the main package
@@ -120,7 +125,7 @@ fn generate_rustdoc_json(package: &str) -> Result<serde_json::Value> {
120125
"-p",
121126
package,
122127
"--target-dir",
123-
"target/rustdoc-json",
128+
&rustdoc_target_dir,
124129
"--",
125130
"-Z",
126131
"unstable-options",
@@ -150,7 +155,7 @@ fn generate_rustdoc_json(package: &str) -> Result<serde_json::Value> {
150155
"-p",
151156
additional_crate,
152157
"--target-dir",
153-
"target/rustdoc-json",
158+
&rustdoc_target_dir,
154159
"--",
155160
"-Z",
156161
"unstable-options",
@@ -180,7 +185,7 @@ fn generate_rustdoc_json(package: &str) -> Result<serde_json::Value> {
180185
};
181186

182187
// Read the generated JSON file - rustdoc generates it based on library name
183-
let json_file_path = format!("target/rustdoc-json/doc/{}.json", lib_name);
188+
let json_file_path = format!("{}/doc/{}.json", rustdoc_target_dir, lib_name);
184189
let json_content = std::fs::read_to_string(json_file_path)
185190
.context("Failed to read generated rustdoc JSON file")?;
186191

@@ -443,7 +448,10 @@ fn parse_field_documentation(
443448
"false" | "no" | "0" => false,
444449
_ => {
445450
// Default to false for invalid values, but could log a warning in the future
446-
eprintln!("Warning: Invalid @required value '{}' for field '{}', defaulting to false", required_text, field_name);
451+
eprintln!(
452+
"Warning: Invalid @required value '{}' for field '{}', defaulting to false",
453+
required_text, field_name
454+
);
447455
false
448456
}
449457
};
@@ -480,7 +488,8 @@ fn parse_literal_block_scalar(lines: &[&str], _base_indent: usize) -> String {
480488
}
481489

482490
// Find the first non-empty content line to determine block indentation
483-
let content_lines: Vec<&str> = lines.iter()
491+
let content_lines: Vec<&str> = lines
492+
.iter()
484493
.skip_while(|line| line.trim().is_empty())
485494
.copied()
486495
.collect();
@@ -531,7 +540,8 @@ fn parse_folded_block_scalar(lines: &[&str], _base_indent: usize) -> String {
531540
}
532541

533542
// Find the first non-empty content line to determine block indentation
534-
let content_lines: Vec<&str> = lines.iter()
543+
let content_lines: Vec<&str> = lines
544+
.iter()
535545
.skip_while(|line| line.trim().is_empty())
536546
.copied()
537547
.collect();
@@ -642,7 +652,11 @@ fn extract_annotation(metadata_section: &str, annotation_name: &str) -> Option<S
642652
if trimmed_after_colon.starts_with('|') {
643653
// Literal block scalar mode (|)
644654
// Content starts from the next line, ignoring any text after | on the same line
645-
let block_lines = collect_annotation_block_lines(&all_lines, annotation_line_idx + 1, annotation_line);
655+
let block_lines = collect_annotation_block_lines(
656+
&all_lines,
657+
annotation_line_idx + 1,
658+
annotation_line,
659+
);
646660

647661
// Convert to owned strings for the parser
648662
let owned_lines: Vec<String> = block_lines.iter().map(|s| s.to_string()).collect();
@@ -659,7 +673,11 @@ fn extract_annotation(metadata_section: &str, annotation_name: &str) -> Option<S
659673
} else if trimmed_after_colon.starts_with('>') {
660674
// Folded block scalar mode (>)
661675
// Content starts from the next line, ignoring any text after > on the same line
662-
let block_lines = collect_annotation_block_lines(&all_lines, annotation_line_idx + 1, annotation_line);
676+
let block_lines = collect_annotation_block_lines(
677+
&all_lines,
678+
annotation_line_idx + 1,
679+
annotation_line,
680+
);
663681

664682
// Convert to owned strings for the parser
665683
let owned_lines: Vec<String> = block_lines.iter().map(|s| s.to_string()).collect();
@@ -684,7 +702,11 @@ fn extract_annotation(metadata_section: &str, annotation_name: &str) -> Option<S
684702
}
685703

686704
// Collect subsequent lines that belong to this annotation
687-
let block_lines = collect_annotation_block_lines(&all_lines, annotation_line_idx + 1, annotation_line);
705+
let block_lines = collect_annotation_block_lines(
706+
&all_lines,
707+
annotation_line_idx + 1,
708+
annotation_line,
709+
);
688710

689711
// For default mode, preserve relative indentation within the block
690712
if !block_lines.is_empty() {
@@ -741,7 +763,7 @@ fn extract_annotation(metadata_section: &str, annotation_name: &str) -> Option<S
741763
fn collect_annotation_block_lines<'a>(
742764
all_lines: &[&'a str],
743765
start_idx: usize,
744-
annotation_line: &str
766+
annotation_line: &str,
745767
) -> Vec<&'a str> {
746768
let mut block_lines = Vec::new();
747769
let annotation_indent = annotation_line.len() - annotation_line.trim_start().len();
@@ -2108,7 +2130,10 @@ and includes various formatting.
21082130
let result = parse_field_documentation(doc_text, "test_field").unwrap();
21092131

21102132
assert_eq!(result.0.name, "test_field");
2111-
assert_eq!(result.0.description, "Field with required and units annotations.");
2133+
assert_eq!(
2134+
result.0.description,
2135+
"Field with required and units annotations."
2136+
);
21122137
assert_eq!(result.0.default_value, Some("`5000`".to_string()));
21132138
assert_eq!(result.0.required, Some(true));
21142139
assert_eq!(result.0.units, Some("milliseconds".to_string()));
@@ -2232,7 +2257,10 @@ and includes various formatting.
22322257
let result = parse_field_documentation(doc_text, "test_field").unwrap();
22332258
let (field_doc, referenced_constants) = result;
22342259

2235-
assert_eq!(field_doc.units, Some("[`DEFAULT_TIMEOUT_MS`] milliseconds".to_string()));
2260+
assert_eq!(
2261+
field_doc.units,
2262+
Some("[`DEFAULT_TIMEOUT_MS`] milliseconds".to_string())
2263+
);
22362264
// Check that constants were collected from units
22372265
assert!(referenced_constants.contains("DEFAULT_TIMEOUT_MS"));
22382266
}
@@ -2402,7 +2430,10 @@ and includes various formatting.
24022430
// Test empty @required annotation (should return None, not Some(false))
24032431
let doc_text_empty = "Test field.\n---\n@required:";
24042432
let result_empty = parse_field_documentation(doc_text_empty, "test_field").unwrap();
2405-
assert_eq!(result_empty.0.required, None, "Empty @required should not be parsed");
2433+
assert_eq!(
2434+
result_empty.0.required, None,
2435+
"Empty @required should not be parsed"
2436+
);
24062437
}
24072438

24082439
#[test]

0 commit comments

Comments
 (0)