Skip to content

Commit 319ee02

Browse files
authored
Improve topic property mappings and filter experimental properties (#154)
* Improve topic property mappings and filter experimental properties - Add dynamic routing for cluster property links (cluster-properties.adoc vs object-storage-properties.adoc) - Support conditional mappings with ternary for alternate cluster properties - Filter topic mappings by exclude_from_docs - Skip experimental properties during extraction (not just in docs) - Don't report experimental properties as removed in diffs - Validate and auto-fix cluster property mappings with _default suffix * Fix tests * Use cluster prop defaults * Use block style * Fix test * Report default updates * Improve whats new format
1 parent 62a0710 commit 319ee02

20 files changed

+2822
-893
lines changed

__tests__/tools/generate-rpcn-connector-docs.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ describe('Utility Helpers', () => {
439439
expect(result).toContain("*Default*: `foo`");
440440
});
441441

442-
it('renders examples for array of strings using YAML flow style', () => {
442+
it('renders examples for array of strings using YAML block style', () => {
443443
const exampleField = [
444444
{
445445
name: 'notes',
@@ -454,12 +454,12 @@ describe('Utility Helpers', () => {
454454
];
455455
const result = renderConnectFields(exampleField).toString();
456456

457-
// 1) Check that "notes:" appears in flow style (with brackets)
457+
// 1) Check that "notes:" appears in block style (with dashes)
458458
// Strings with whitespace should be quoted
459-
expect(result).toMatch(/notes: \["single line note", "another single note"\]/);
459+
expect(result).toMatch(/notes:\s+- "single line note"\s+- "another single note"/);
460460

461-
// 2) Check that multi-line strings are rendered in flow style and quoted
462-
expect(result).toMatch(/notes: \["multi[\s\S]*line[\s\S]*note"\]/);
461+
// 2) Check that multi-line strings are rendered in block style and quoted
462+
expect(result).toMatch(/notes:\s+- "multi[\s\S]*line[\s\S]*note"/);
463463
});
464464

465465
it('renders a list of example configs in renderConnectExamples', () => {

__tests__/tools/topic_property_extractor.test.js

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('topic_property_extractor.py', () => {
77
const mockSourcePath = path.resolve(__dirname, 'mock-redpanda-src');
88
const outputJson = path.resolve(__dirname, 'topic-properties-output.json');
99
const outputAdoc = path.resolve(__dirname, 'topic-properties.adoc');
10+
const clusterPropsJson = path.resolve(__dirname, 'mock-cluster-properties.json');
1011

1112
beforeAll(() => {
1213
// Create a minimal mock Redpanda source tree
@@ -20,6 +21,7 @@ describe('topic_property_extractor.py', () => {
2021
`inline constexpr std::string_view topic_property_retention_ms = "retention.ms";
2122
inline constexpr std::string_view topic_property_segment_bytes = "segment.bytes";
2223
inline constexpr std::string_view topic_property_flush_messages = "flush.messages";
24+
inline constexpr std::string_view topic_property_no_mapping = "redpanda.no.mapping";
2325
2426
// Mock allowlist for no-op properties
2527
inline constexpr std::array<std::string_view, 3> allowlist_topic_noop_confs = {
@@ -34,33 +36,78 @@ inline constexpr std::array<std::string_view, 3> allowlist_topic_noop_confs = {
3436
path.join(headerDir, 'types.cc'),
3537
`// Copyright 2025 Redpanda Data, Inc.\n#include "kafka/server/handlers/topics/types.h"\n// ...rest of the file...\n`
3638
);
37-
// Add a mock config file to simulate a cluster property mapping
38-
const configDir = path.join(mockSourcePath, 'src/v/config');
39+
// Add a mock config_response_utils.cc file to simulate cluster property mappings
40+
const configDir = path.join(mockSourcePath, 'src/v/kafka/server/handlers/configs');
3941
fs.mkdirSync(configDir, { recursive: true });
4042
fs.writeFileSync(
41-
path.join(configDir, 'mock_config.cc'),
42-
'config.get("log_retention_ms");\nconfig.get("log_segment_size");\n'
43+
path.join(configDir, 'config_response_utils.cc'),
44+
`// Mock config response utils
45+
add_topic_config_if_requested(
46+
topic_property_retention_ms,
47+
config::shard_local_cfg().log_retention_ms.name(),
48+
config::shard_local_cfg().log_retention_ms.desc()
49+
);
50+
51+
add_topic_config_if_requested(
52+
topic_property_segment_bytes,
53+
config::shard_local_cfg().log_segment_size.name(),
54+
config::shard_local_cfg().log_segment_size.desc()
55+
);
56+
57+
add_topic_config_if_requested(
58+
topic_property_flush_messages,
59+
config::shard_local_cfg().flush_messages.name(),
60+
config::shard_local_cfg().flush_messages.desc()
61+
);
62+
`
4363
);
64+
65+
// Create mock cluster properties JSON with default values
66+
const mockClusterProps = {
67+
properties: {
68+
log_retention_ms: {
69+
name: 'log_retention_ms',
70+
type: 'integer',
71+
default: 604800000,
72+
default_human_readable: '7 days',
73+
description: 'Retention time in milliseconds'
74+
},
75+
log_segment_size: {
76+
name: 'log_segment_size',
77+
type: 'integer',
78+
default: 1073741824,
79+
description: 'Segment size in bytes'
80+
},
81+
flush_messages: {
82+
name: 'flush_messages',
83+
type: 'integer',
84+
default: 100000,
85+
description: 'Number of messages before flush'
86+
}
87+
}
88+
};
89+
fs.writeFileSync(clusterPropsJson, JSON.stringify(mockClusterProps, null, 2));
4490
}
4591
});
4692

4793
afterAll(() => {
4894
// Cleanup
4995
if (fs.existsSync(outputJson)) fs.unlinkSync(outputJson);
5096
if (fs.existsSync(outputAdoc)) fs.unlinkSync(outputAdoc);
97+
if (fs.existsSync(clusterPropsJson)) fs.unlinkSync(clusterPropsJson);
5198
fs.rmdirSync(mockSourcePath, { recursive: true });
5299
});
53100

54101
it('extracts topic properties and generates JSON', () => {
55-
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-json ${outputJson}`);
102+
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-json ${outputJson} --cluster-properties-json ${clusterPropsJson}`);
56103
const result = JSON.parse(fs.readFileSync(outputJson, 'utf8'));
57104
expect(result.topic_properties).toBeDefined();
58105
expect(result.topic_properties['retention.ms']).toBeDefined();
59106
expect(result.topic_properties['retention.ms'].property_name).toBe('retention.ms');
60107
});
61108

62109
it('detects no-op properties correctly', () => {
63-
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-json ${outputJson}`);
110+
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-json ${outputJson} --cluster-properties-json ${clusterPropsJson}`);
64111
const result = JSON.parse(fs.readFileSync(outputJson, 'utf8'));
65112

66113
// Check that noop_properties array is present
@@ -81,17 +128,71 @@ inline constexpr std::array<std::string_view, 3> allowlist_topic_noop_confs = {
81128
});
82129

83130
it('excludes no-op properties from AsciiDoc generation', () => {
84-
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-adoc ${outputAdoc}`);
131+
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-adoc ${outputAdoc} --cluster-properties-json ${clusterPropsJson}`);
85132
const adoc = fs.readFileSync(outputAdoc, 'utf8');
86-
87-
// Should contain regular properties
133+
134+
// Should contain regular properties in the documentation
88135
expect(adoc).toContain('= Topic Configuration Properties');
89136
expect(adoc).toContain('retention.ms');
90137
expect(adoc).toContain('segment.bytes');
91-
138+
92139
// Should NOT contain no-op properties in documentation
93140
expect(adoc).not.toContain('flush.messages');
94141
expect(adoc).not.toContain('segment.index.bytes');
95142
expect(adoc).not.toContain('preallocate');
143+
144+
// Properties with cluster mappings should appear in the mappings table
145+
expect(adoc).toMatch(/Topic property mappings[\s\S]*retention\.ms[\s\S]*log_retention_ms/);
146+
expect(adoc).toMatch(/Topic property mappings[\s\S]*segment\.bytes[\s\S]*log_segment_size/);
147+
});
148+
149+
it('documents properties without cluster mappings', () => {
150+
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-adoc ${outputAdoc} --cluster-properties-json ${clusterPropsJson}`);
151+
const adoc = fs.readFileSync(outputAdoc, 'utf8');
152+
153+
// Property without cluster mapping should appear in the main documentation
154+
expect(adoc).toContain('redpanda.no.mapping');
155+
156+
// Extract the mappings table section (between "Topic property mappings" and "== Topic properties")
157+
const mappingsSection = adoc.match(/Topic property mappings[\s\S]*?(?===\s+Topic properties)/);
158+
159+
// Property without mapping should NOT appear in the mappings table
160+
if (mappingsSection) {
161+
expect(mappingsSection[0]).not.toContain('redpanda.no.mapping');
162+
}
163+
164+
// But it should appear in the Topic properties section
165+
const propertiesSection = adoc.match(/==\s+Topic properties[\s\S]*/);
166+
expect(propertiesSection[0]).toContain('redpanda.no.mapping');
167+
168+
// And should not have a "Related cluster property" line
169+
const noMappingSection = adoc.match(/===\s+redpanda\.no\.mapping[\s\S]*?---/);
170+
expect(noMappingSection).toBeTruthy();
171+
expect(noMappingSection[0]).not.toContain('Related cluster property');
172+
});
173+
174+
it('populates default values from cluster properties', () => {
175+
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-json ${outputJson} --cluster-properties-json ${clusterPropsJson}`);
176+
const result = JSON.parse(fs.readFileSync(outputJson, 'utf8'));
177+
178+
// Check that default values are populated from cluster properties
179+
expect(result.topic_properties['retention.ms'].default).toBe(604800000);
180+
expect(result.topic_properties['retention.ms'].default_human_readable).toBe('7 days');
181+
expect(result.topic_properties['segment.bytes'].default).toBe(1073741824);
182+
183+
// Property without cluster mapping should not have a default
184+
expect(result.topic_properties['redpanda.no.mapping'].default).toBeNull();
185+
});
186+
187+
it('populates default values in JSON for use by Handlebars templates', () => {
188+
// The Python script doesn't format defaults in AsciiDoc - that's handled by Handlebars
189+
// But it should populate the default field in the JSON output
190+
execSync(`python3 ${scriptPath} --source-path ${mockSourcePath} --output-json ${outputJson} --cluster-properties-json ${clusterPropsJson}`);
191+
const result = JSON.parse(fs.readFileSync(outputJson, 'utf8'));
192+
193+
// Verify the JSON has raw default values that Handlebars will format
194+
expect(result.topic_properties['retention.ms'].default).toBe(604800000);
195+
expect(result.topic_properties['retention.ms'].default_human_readable).toBe('7 days');
196+
expect(result.topic_properties['segment.bytes'].default).toBe(1073741824);
96197
});
97198
});

0 commit comments

Comments
 (0)