@redpanda-data/docs-extensions-and-macros 4.11.1 โ 4.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +201 -10
- package/package.json +3 -1
- package/tools/property-extractor/COMPUTED_CONSTANTS.md +173 -0
- package/tools/property-extractor/Makefile +12 -1
- package/tools/property-extractor/README.adoc +828 -97
- package/tools/property-extractor/compare-properties.js +38 -13
- package/tools/property-extractor/constant_resolver.py +610 -0
- package/tools/property-extractor/file_pair.py +42 -0
- package/tools/property-extractor/generate-handlebars-docs.js +41 -8
- package/tools/property-extractor/helpers/gt.js +9 -0
- package/tools/property-extractor/helpers/includes.js +17 -0
- package/tools/property-extractor/helpers/index.js +3 -0
- package/tools/property-extractor/helpers/isEnterpriseEnum.js +24 -0
- package/tools/property-extractor/helpers/renderPropertyExample.js +6 -5
- package/tools/property-extractor/overrides.json +248 -0
- package/tools/property-extractor/parser.py +254 -32
- package/tools/property-extractor/property_bag.py +40 -0
- package/tools/property-extractor/property_extractor.py +1417 -430
- package/tools/property-extractor/requirements.txt +1 -0
- package/tools/property-extractor/templates/property-backup.hbs +161 -0
- package/tools/property-extractor/templates/property.hbs +104 -49
- package/tools/property-extractor/templates/topic-property-backup.hbs +148 -0
- package/tools/property-extractor/templates/topic-property.hbs +72 -34
- package/tools/property-extractor/tests/test_known_values.py +617 -0
- package/tools/property-extractor/tests/transformers_test.py +81 -6
- package/tools/property-extractor/topic_property_extractor.py +23 -10
- package/tools/property-extractor/transformers.py +2191 -369
- package/tools/property-extractor/type_definition_extractor.py +669 -0
- package/tools/redpanda-connect/helpers/renderConnectFields.js +33 -1
- package/tools/redpanda-connect/report-delta.js +132 -9
- package/tools/property-extractor/definitions.json +0 -245
package/bin/doc-tools.js
CHANGED
|
@@ -719,6 +719,18 @@ automation
|
|
|
719
719
|
fs.unlinkSync(tmpFile);
|
|
720
720
|
dataFile = finalFile;
|
|
721
721
|
console.log(`โ
Fetched and saved: ${finalFile}`);
|
|
722
|
+
|
|
723
|
+
// Keep only 2 most recent versions in docs-data
|
|
724
|
+
const dataFiles = fs.readdirSync(dataDir)
|
|
725
|
+
.filter(f => /^connect-\d+\.\d+\.\d+\.json$/.test(f))
|
|
726
|
+
.sort();
|
|
727
|
+
|
|
728
|
+
while (dataFiles.length > 2) {
|
|
729
|
+
const oldestFile = dataFiles.shift();
|
|
730
|
+
const oldestPath = path.join(dataDir, oldestFile);
|
|
731
|
+
fs.unlinkSync(oldestPath);
|
|
732
|
+
console.log(`๐งน Deleted old version from docs-data: ${oldestFile}`);
|
|
733
|
+
}
|
|
722
734
|
} catch (err) {
|
|
723
735
|
console.error(`โ Failed to fetch connectors: ${err.message}`);
|
|
724
736
|
process.exit(1);
|
|
@@ -845,6 +857,45 @@ automation
|
|
|
845
857
|
}
|
|
846
858
|
|
|
847
859
|
const newIndex = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
|
|
860
|
+
|
|
861
|
+
// Publish merged version with overrides to modules/components/attachments
|
|
862
|
+
if (options.overrides && fs.existsSync(options.overrides)) {
|
|
863
|
+
try {
|
|
864
|
+
const { mergeOverrides, resolveReferences } = require('../tools/redpanda-connect/generate-rpcn-connector-docs.js');
|
|
865
|
+
|
|
866
|
+
// Create a copy of newIndex to merge overrides into
|
|
867
|
+
const mergedData = JSON.parse(JSON.stringify(newIndex));
|
|
868
|
+
|
|
869
|
+
// Read and apply overrides
|
|
870
|
+
const ovRaw = fs.readFileSync(options.overrides, 'utf8');
|
|
871
|
+
const ovObj = JSON.parse(ovRaw);
|
|
872
|
+
const resolvedOverrides = resolveReferences(ovObj, ovObj);
|
|
873
|
+
mergeOverrides(mergedData, resolvedOverrides);
|
|
874
|
+
|
|
875
|
+
// Publish to modules/components/attachments
|
|
876
|
+
const attachmentsRoot = path.resolve(process.cwd(), 'modules/components/attachments');
|
|
877
|
+
fs.mkdirSync(attachmentsRoot, { recursive: true });
|
|
878
|
+
|
|
879
|
+
// Delete older versions from modules/components/attachments
|
|
880
|
+
const existingFiles = fs.readdirSync(attachmentsRoot)
|
|
881
|
+
.filter(f => /^connect-\d+\.\d+\.\d+\.json$/.test(f))
|
|
882
|
+
.sort();
|
|
883
|
+
|
|
884
|
+
for (const oldFile of existingFiles) {
|
|
885
|
+
const oldFilePath = path.join(attachmentsRoot, oldFile);
|
|
886
|
+
fs.unlinkSync(oldFilePath);
|
|
887
|
+
console.log(`๐งน Deleted old version: ${oldFile}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Save merged version to modules/components/attachments
|
|
891
|
+
const destFile = path.join(attachmentsRoot, `connect-${newVersion}.json`);
|
|
892
|
+
fs.writeFileSync(destFile, JSON.stringify(mergedData, null, 2), 'utf8');
|
|
893
|
+
console.log(`โ
Published merged version to: ${path.relative(process.cwd(), destFile)}`);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
console.error(`โ Failed to publish merged version: ${err.message}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
848
899
|
printDeltaReport(oldIndex, newIndex);
|
|
849
900
|
|
|
850
901
|
// Generate JSON diff file for whats-new.adoc
|
|
@@ -895,6 +946,93 @@ automation
|
|
|
895
946
|
|
|
896
947
|
// Optionally update whats-new.adoc
|
|
897
948
|
if (options.updateWhatsNew) {
|
|
949
|
+
// Helper function to cap description to two sentences
|
|
950
|
+
const capToTwoSentences = (description) => {
|
|
951
|
+
if (!description) return '';
|
|
952
|
+
|
|
953
|
+
// Helper to check if text contains problematic content
|
|
954
|
+
const hasProblematicContent = (text) => {
|
|
955
|
+
return /```[\s\S]*?```/.test(text) || // code blocks
|
|
956
|
+
/`[^`]+`/.test(text) || // inline code
|
|
957
|
+
/^[=#]+\s+.+$/m.test(text) || // headings
|
|
958
|
+
/\n/.test(text); // newlines
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// Step 1: Replace common abbreviations and ellipses with placeholders
|
|
962
|
+
const abbreviations = [
|
|
963
|
+
/\bv\d+\.\d+(?:\.\d+)?/gi, // version numbers like v4.12 or v4.12.0 (must come before decimal)
|
|
964
|
+
/\d+\.\d+/g, // decimal numbers
|
|
965
|
+
/\be\.g\./gi, // e.g.
|
|
966
|
+
/\bi\.e\./gi, // i.e.
|
|
967
|
+
/\betc\./gi, // etc.
|
|
968
|
+
/\bvs\./gi, // vs.
|
|
969
|
+
/\bDr\./gi, // Dr.
|
|
970
|
+
/\bMr\./gi, // Mr.
|
|
971
|
+
/\bMs\./gi, // Ms.
|
|
972
|
+
/\bMrs\./gi, // Mrs.
|
|
973
|
+
/\bSt\./gi, // St.
|
|
974
|
+
/\bNo\./gi // No.
|
|
975
|
+
];
|
|
976
|
+
|
|
977
|
+
let normalized = description;
|
|
978
|
+
const placeholders = [];
|
|
979
|
+
|
|
980
|
+
// Replace abbreviations with placeholders
|
|
981
|
+
abbreviations.forEach((abbrevRegex, idx) => {
|
|
982
|
+
normalized = normalized.replace(abbrevRegex, (match) => {
|
|
983
|
+
const placeholder = `__ABBREV${idx}_${placeholders.length}__`;
|
|
984
|
+
placeholders.push({ placeholder, original: match });
|
|
985
|
+
return placeholder;
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// Replace ellipses (three or more dots) with placeholder
|
|
990
|
+
normalized = normalized.replace(/\.{3,}/g, (match) => {
|
|
991
|
+
const placeholder = `__ELLIPSIS_${placeholders.length}__`;
|
|
992
|
+
placeholders.push({ placeholder, original: match });
|
|
993
|
+
return placeholder;
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Step 2: Split sentences using the regex
|
|
997
|
+
const sentenceRegex = /[^.!?]+[.!?]+(?:\s|$)/g;
|
|
998
|
+
const sentences = normalized.match(sentenceRegex);
|
|
999
|
+
|
|
1000
|
+
if (!sentences || sentences.length === 0) {
|
|
1001
|
+
// Restore placeholders and return original
|
|
1002
|
+
let result = normalized;
|
|
1003
|
+
placeholders.forEach(({ placeholder, original }) => {
|
|
1004
|
+
result = result.replace(placeholder, original);
|
|
1005
|
+
});
|
|
1006
|
+
return result;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Step 3: Determine how many sentences to include
|
|
1010
|
+
let maxSentences = 2;
|
|
1011
|
+
|
|
1012
|
+
// If we have at least 2 sentences, check if the second one has problematic content
|
|
1013
|
+
if (sentences.length >= 2) {
|
|
1014
|
+
// Restore placeholders in second sentence to check original content
|
|
1015
|
+
let secondSentence = sentences[1];
|
|
1016
|
+
placeholders.forEach(({ placeholder, original }) => {
|
|
1017
|
+
secondSentence = secondSentence.replace(new RegExp(placeholder, 'g'), original);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// If second sentence has problematic content, only take first sentence
|
|
1021
|
+
if (hasProblematicContent(secondSentence)) {
|
|
1022
|
+
maxSentences = 1;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
let result = sentences.slice(0, maxSentences).join('');
|
|
1027
|
+
|
|
1028
|
+
// Step 4: Restore placeholders back to original text
|
|
1029
|
+
placeholders.forEach(({ placeholder, original }) => {
|
|
1030
|
+
result = result.replace(new RegExp(placeholder, 'g'), original);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
return result.trim();
|
|
1034
|
+
};
|
|
1035
|
+
|
|
898
1036
|
try {
|
|
899
1037
|
const whatsNewPath = path.join(findRepoRoot(), 'modules/get-started/pages/whats-new.adoc');
|
|
900
1038
|
if (!fs.existsSync(whatsNewPath)) {
|
|
@@ -956,7 +1094,7 @@ automation
|
|
|
956
1094
|
for (const comp of comps) {
|
|
957
1095
|
section += `** xref:components:${type}/${comp.name}.adoc[\`${comp.name}\`]`;
|
|
958
1096
|
if (comp.status) section += ` (${comp.status})`;
|
|
959
|
-
if (comp.description) section += `: ${comp.description}`;
|
|
1097
|
+
if (comp.description) section += `: ${capToTwoSentences(comp.description)}`;
|
|
960
1098
|
section += '\n';
|
|
961
1099
|
}
|
|
962
1100
|
}
|
|
@@ -977,6 +1115,60 @@ automation
|
|
|
977
1115
|
description: field.description || '',
|
|
978
1116
|
});
|
|
979
1117
|
}
|
|
1118
|
+
for (const [type, fields] of Object.entries(fieldsByType)) {
|
|
1119
|
+
section += `* ${type.charAt(0).toUpperCase() + type.slice(1)}:\n`;
|
|
1120
|
+
// Group by component name
|
|
1121
|
+
const byComp = {};
|
|
1122
|
+
for (const f of fields) {
|
|
1123
|
+
if (!byComp[f.compName]) byComp[f.compName] = [];
|
|
1124
|
+
byComp[f.compName].push(f);
|
|
1125
|
+
}
|
|
1126
|
+
for (const [comp, compFields] of Object.entries(byComp)) {
|
|
1127
|
+
section += `** xref:components:${type}/${comp}.adoc[\`${comp}\`]:`;
|
|
1128
|
+
section += '\n';
|
|
1129
|
+
for (const f of compFields) {
|
|
1130
|
+
section += `*** xref:components:${type}/${comp}.adoc#${f.field}[\`${f.field}\`]`;
|
|
1131
|
+
if (f.description) section += `: ${capToTwoSentences(f.description)}`;
|
|
1132
|
+
section += '\n';
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Deprecated components
|
|
1139
|
+
if (diff.details.deprecatedComponents && diff.details.deprecatedComponents.length) {
|
|
1140
|
+
section += '\n=== Deprecations\n\n';
|
|
1141
|
+
section += 'The following components are now deprecated:\n\n';
|
|
1142
|
+
// Group by type
|
|
1143
|
+
const byType = {};
|
|
1144
|
+
for (const comp of diff.details.deprecatedComponents) {
|
|
1145
|
+
if (!byType[comp.type]) byType[comp.type] = [];
|
|
1146
|
+
byType[comp.type].push(comp);
|
|
1147
|
+
}
|
|
1148
|
+
for (const [type, comps] of Object.entries(byType)) {
|
|
1149
|
+
section += `* ${type.charAt(0).toUpperCase() + type.slice(1)}:\n`;
|
|
1150
|
+
for (const comp of comps) {
|
|
1151
|
+
section += `** xref:components:${type}/${comp.name}.adoc[\`${comp.name}\`]\n`;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Deprecated fields
|
|
1157
|
+
if (diff.details.deprecatedFields && diff.details.deprecatedFields.length) {
|
|
1158
|
+
if (!diff.details.deprecatedComponents || diff.details.deprecatedComponents.length === 0) {
|
|
1159
|
+
section += '\n=== Deprecations\n\n';
|
|
1160
|
+
}
|
|
1161
|
+
section += '\nThe following fields are now deprecated:\n\n';
|
|
1162
|
+
// Group deprecated fields by component type
|
|
1163
|
+
const fieldsByType = {};
|
|
1164
|
+
for (const field of diff.details.deprecatedFields) {
|
|
1165
|
+
const [type, compName] = field.component.split(':');
|
|
1166
|
+
if (!fieldsByType[type]) fieldsByType[type] = [];
|
|
1167
|
+
fieldsByType[type].push({
|
|
1168
|
+
compName,
|
|
1169
|
+
field: field.field
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
980
1172
|
for (const [type, fields] of Object.entries(fieldsByType)) {
|
|
981
1173
|
section += `* ${type.charAt(0).toUpperCase() + type.slice(1)} components\n`;
|
|
982
1174
|
// Group by component name
|
|
@@ -986,23 +1178,20 @@ automation
|
|
|
986
1178
|
byComp[f.compName].push(f);
|
|
987
1179
|
}
|
|
988
1180
|
for (const [comp, compFields] of Object.entries(byComp)) {
|
|
989
|
-
section += `** xref:components:${type}/${comp}.adoc[
|
|
1181
|
+
section += `** xref:components:${type}/${comp}.adoc[\`${comp}\`]`;
|
|
990
1182
|
if (compFields.length === 1) {
|
|
991
1183
|
const f = compFields[0];
|
|
992
|
-
section += `: xref:components:${type}/${comp}.adoc#${f.field}[
|
|
993
|
-
if (f.description) section += ` - ${f.description}`;
|
|
994
|
-
section += '\n';
|
|
1184
|
+
section += `: xref:components:${type}/${comp}.adoc#${f.field}[\`${f.field}\`]\n`;
|
|
995
1185
|
} else {
|
|
996
1186
|
section += '\n';
|
|
997
1187
|
for (const f of compFields) {
|
|
998
|
-
section += `*** xref:components:${type}/${comp}.adoc#${f.field}[
|
|
999
|
-
if (f.description) section += ` - ${f.description}`;
|
|
1000
|
-
section += '\n';
|
|
1188
|
+
section += `*** xref:components:${type}/${comp}.adoc#${f.field}[\`${f.field}\`]\n`;
|
|
1001
1189
|
}
|
|
1002
1190
|
}
|
|
1003
1191
|
}
|
|
1004
1192
|
}
|
|
1005
1193
|
}
|
|
1194
|
+
|
|
1006
1195
|
let updated;
|
|
1007
1196
|
if (startIdx !== -1) {
|
|
1008
1197
|
// Replace the existing section
|
|
@@ -1148,8 +1337,10 @@ automation
|
|
|
1148
1337
|
|
|
1149
1338
|
// If we used Antora's latest-redpanda-tag for diff, update it to the new tag
|
|
1150
1339
|
if (!options.diff && !tagsAreSame) {
|
|
1151
|
-
setAntoraValue('asciidoc.attributes.latest-redpanda-tag', newTag);
|
|
1152
|
-
|
|
1340
|
+
const success = setAntoraValue('asciidoc.attributes.latest-redpanda-tag', newTag);
|
|
1341
|
+
if (success) {
|
|
1342
|
+
console.log(`โ
Updated Antora latest-redpanda-tag to: ${newTag}`);
|
|
1343
|
+
}
|
|
1153
1344
|
}
|
|
1154
1345
|
|
|
1155
1346
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.1",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"build": "antora --to-dir docs --fetch local-antora-playbook.yml",
|
|
23
23
|
"serve": "wds --node-resolve --open preview/test/ --watch --root-dir docs",
|
|
24
24
|
"test": "jest",
|
|
25
|
+
"test:python": "./__tests__/tools/property-extractor/setup-and-test.sh",
|
|
26
|
+
"test:all": "npm run test && npm run test:python",
|
|
25
27
|
"bundle:admin": "doc-tools generate bundle-openapi --surface admin",
|
|
26
28
|
"bundle:connect": "doc-tools generate bundle-openapi --surface connect",
|
|
27
29
|
"bundle:both": "doc-tools generate bundle-openapi --surface both"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Computed C++ Constants Resolution
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Some C++ constants in Redpanda are defined with complex compile-time expressions that cannot be easily parsed by the property-extractor. These constants need to be pre-computed and mapped to their actual values.
|
|
6
|
+
|
|
7
|
+
## Problem Statement
|
|
8
|
+
|
|
9
|
+
Properties like `log_message_timestamp_before_max_ms` use constants like `max_serializable_ms` as their default values. These constants are defined with complex expressions:
|
|
10
|
+
|
|
11
|
+
```cpp
|
|
12
|
+
// From src/v/serde/rw/chrono.h:20
|
|
13
|
+
inline constexpr auto max_serializable_ms
|
|
14
|
+
= std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
15
|
+
std::chrono::nanoseconds::max());
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Without resolution, the extracted schema would show:
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"log_message_timestamp_before_max_ms": {
|
|
22
|
+
"default": "max_serializable_ms" // โ String instead of numeric value
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Solution
|
|
28
|
+
|
|
29
|
+
### 1. COMPUTED_CONSTANTS Dictionary
|
|
30
|
+
|
|
31
|
+
Added a dictionary in `transformers.py` that maps constant names to their computed values:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
COMPUTED_CONSTANTS = {
|
|
35
|
+
# From src/v/serde/rw/chrono.h:20
|
|
36
|
+
# inline constexpr auto max_serializable_ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::nanoseconds::max());
|
|
37
|
+
# Calculation: std::numeric_limits<int64_t>::max() / 1,000,000 = 9223372036854775807 / 1000000 = 9223372036854 ms
|
|
38
|
+
"max_serializable_ms": 9223372036854, # ~292 years in milliseconds
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. FriendlyDefaultTransformer Enhancement
|
|
43
|
+
|
|
44
|
+
Updated the `FriendlyDefaultTransformer` to check the `COMPUTED_CONSTANTS` dictionary before falling back to string normalization:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Computed C++ constants (max_serializable_ms, etc.)
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
if d in COMPUTED_CONSTANTS:
|
|
51
|
+
property["default"] = COMPUTED_CONSTANTS[d]
|
|
52
|
+
return property
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Test Coverage
|
|
56
|
+
|
|
57
|
+
Added comprehensive test in `tests/test_known_values.py`:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
def test_max_serializable_ms_constant_resolution(self):
|
|
61
|
+
"""Test that max_serializable_ms constant is resolved to actual numeric value"""
|
|
62
|
+
info = create_complete_property_info(
|
|
63
|
+
name="log_message_timestamp_before_max_ms",
|
|
64
|
+
description="Maximum timestamp difference for record validation",
|
|
65
|
+
declaration="property<std::chrono::milliseconds> log_message_timestamp_before_max_ms;",
|
|
66
|
+
metadata="meta{.needs_restart = needs_restart::no, .visibility = visibility::user}",
|
|
67
|
+
default_value="max_serializable_ms"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
property = apply_transformer_pipeline(info)
|
|
71
|
+
|
|
72
|
+
self.assertEqual(property["name"], "log_message_timestamp_before_max_ms")
|
|
73
|
+
self.assertEqual(property["type"], "integer")
|
|
74
|
+
# max_serializable_ms = std::numeric_limits<int64_t>::max() / 1,000,000 = 9223372036854 ms
|
|
75
|
+
self.assertEqual(property["default"], 9223372036854)
|
|
76
|
+
self.assertFalse(property["needs_restart"])
|
|
77
|
+
self.assertEqual(property["visibility"], "user")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Calculation Details
|
|
81
|
+
|
|
82
|
+
### max_serializable_ms
|
|
83
|
+
|
|
84
|
+
**Definition Location:** `src/v/serde/rw/chrono.h:20`
|
|
85
|
+
|
|
86
|
+
**C++ Expression:**
|
|
87
|
+
```cpp
|
|
88
|
+
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
89
|
+
std::chrono::nanoseconds::max()
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Calculation:**
|
|
94
|
+
1. `std::chrono::nanoseconds::max()` = `std::numeric_limits<int64_t>::max()` = `9223372036854775807` nanoseconds
|
|
95
|
+
2. Convert to milliseconds (truncating division): `9223372036854775807 / 1000000` = `9223372036854` milliseconds
|
|
96
|
+
3. This is approximately **292.47 years**
|
|
97
|
+
|
|
98
|
+
**Verification:**
|
|
99
|
+
```python
|
|
100
|
+
import sys
|
|
101
|
+
|
|
102
|
+
# std::chrono::nanoseconds uses int64_t for rep
|
|
103
|
+
max_int64 = 9223372036854775807
|
|
104
|
+
|
|
105
|
+
# Convert nanoseconds to milliseconds (duration_cast truncates)
|
|
106
|
+
max_ms = max_int64 // 1000000
|
|
107
|
+
|
|
108
|
+
print(f'max_serializable_ms = {max_ms} ms')
|
|
109
|
+
print(f'Which is approximately {max_ms / (1000 * 60 * 60 * 24 * 365):.2f} years')
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Output:
|
|
113
|
+
```
|
|
114
|
+
max_serializable_ms = 9223372036854 ms
|
|
115
|
+
Which is approximately 292.47 years
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Result
|
|
119
|
+
|
|
120
|
+
After the fix, the extracted schema correctly shows:
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"log_message_timestamp_before_max_ms": {
|
|
124
|
+
"default": 9223372036854, // โ
Correct numeric value
|
|
125
|
+
"type": "integer"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Adding New Computed Constants
|
|
131
|
+
|
|
132
|
+
To add support for new computed constants:
|
|
133
|
+
|
|
134
|
+
1. **Find the definition** in the Redpanda source code
|
|
135
|
+
2. **Compute the value** - either manually or with a Python/C++ test
|
|
136
|
+
3. **Add to COMPUTED_CONSTANTS** dictionary in `transformers.py`:
|
|
137
|
+
```python
|
|
138
|
+
COMPUTED_CONSTANTS = {
|
|
139
|
+
"existing_constant": 12345,
|
|
140
|
+
"new_constant_name": computed_value, # Add comment with source location
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
4. **Add a test** in `tests/test_known_values.py` to verify resolution
|
|
144
|
+
5. **Document the calculation** with comments including:
|
|
145
|
+
- Source file location
|
|
146
|
+
- C++ expression
|
|
147
|
+
- Calculation steps
|
|
148
|
+
- Human-readable interpretation
|
|
149
|
+
|
|
150
|
+
## Test Coverage
|
|
151
|
+
|
|
152
|
+
All 53 tests pass, including the new test for `max_serializable_ms` resolution:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
cd tools/property-extractor
|
|
156
|
+
python -m pytest tests/ -v --tb=short
|
|
157
|
+
# ================================================= 53 passed =================================================
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Benefits
|
|
161
|
+
|
|
162
|
+
โ
**Accurate defaults** - Properties show actual numeric values instead of symbolic names
|
|
163
|
+
โ
**Type safety** - Numeric properties have numeric defaults, not strings
|
|
164
|
+
โ
**Documentation quality** - Users see real values they can use
|
|
165
|
+
โ
**Maintainability** - Centralized mapping makes updates easy
|
|
166
|
+
โ
**Test coverage** - Ensures constants resolve correctly
|
|
167
|
+
|
|
168
|
+
## Related Files
|
|
169
|
+
|
|
170
|
+
- `tools/property-extractor/transformers.py` - COMPUTED_CONSTANTS dictionary and resolution logic
|
|
171
|
+
- `tools/property-extractor/tests/test_known_values.py` - Test for constant resolution
|
|
172
|
+
- `src/v/serde/rw/chrono.h` - Source definition of max_serializable_ms
|
|
173
|
+
- `tools/property-extractor/CI_INTEGRATION.md` - Updated test count documentation
|
|
@@ -119,7 +119,18 @@ generate-docs: node-deps
|
|
|
119
119
|
fi
|
|
120
120
|
@echo "๐ Copying properties JSON files to $(OUTPUT_JSON_DIR)โฆ"
|
|
121
121
|
@if [ -f "$(TOOL_ROOT)/gen/$(TAG)-properties.json" ]; then \
|
|
122
|
-
|
|
122
|
+
echo " Source: $(TOOL_ROOT)/gen/$(TAG)-properties.json"; \
|
|
123
|
+
echo " Target: $(OUTPUT_JSON_DIR)/$(TAG)-properties.json"; \
|
|
124
|
+
cp "$(TOOL_ROOT)/gen/$(TAG)-properties.json" "$(OUTPUT_JSON_DIR)/" && \
|
|
125
|
+
echo " โ
Successfully copied $(TAG)-properties.json" || \
|
|
126
|
+
{ echo " โ Failed to copy $(TAG)-properties.json"; exit 1; }; \
|
|
127
|
+
if [ -f "$(OUTPUT_JSON_DIR)/$(TAG)-properties.json" ]; then \
|
|
128
|
+
echo " ๐ Verification: Target file exists and has $$(wc -c < "$(OUTPUT_JSON_DIR)/$(TAG)-properties.json") bytes"; \
|
|
129
|
+
else \
|
|
130
|
+
echo " โ Verification failed: Target file does not exist"; exit 1; \
|
|
131
|
+
fi; \
|
|
132
|
+
else \
|
|
133
|
+
echo " โ ๏ธ Source file $(TOOL_ROOT)/gen/$(TAG)-properties.json not found, skipping copy"; \
|
|
123
134
|
fi
|
|
124
135
|
@echo "โ
Docs generated at $(OUTPUT_AUTOGENERATED_DIR)"
|
|
125
136
|
|