@redpanda-data/docs-extensions-and-macros 4.12.3 → 4.12.5

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.
@@ -80,24 +80,45 @@ Redpanda configuration properties are C++ objects with constructor signatures th
80
80
  based on feature requirements. Understanding these "arities" (parameter counts) is crucial
81
81
  for correctly extracting property metadata.
82
82
 
83
+ ┌─────────────────────────────────────────────────────────────────────────────
84
+ │ ALL properties have a runtime mutability flag as their FIRST parameter
85
+ │ (after *this which is skipped by the parser). This boolean indicates whether
86
+ │ the property can be changed at runtime without restarting Redpanda.
87
+
88
+ │ C++ signature: property<T>(*this, runtime_mutable, name, description, ...)
89
+ │ After parsing: params[0] = runtime_mutable, params[1] = name, ...
90
+ │ params[-1] = default OR validator (validator is skipped)
91
+
92
+ │ Note: Some properties include validators as the last parameter. These are
93
+ │ automatically detected and skipped when extracting the default value.
94
+ │ The default is the last NON-VALIDATOR parameter.
95
+ └─────────────────────────────────────────────────────────────────────────────
96
+
83
97
  BASIC PROPERTY PATTERNS:
84
98
  ┌─────────────────────────────────────────────────────────────────────────────
85
- 2-PARAMETER: property<T>(name, description)
86
- │ Example: property<bool>(*this, "enable_feature", "Enable the feature")
99
+ 3-PARAMETER: property<T>(runtime_mutable, name, description)
100
+ │ Example: property<bool>(*this, true, "enable_feature", "Enable the feature")
87
101
  │ Used for: Simple properties with no metadata or custom defaults
88
- │ Extraction: [0] = name, [1] = description
102
+ │ Extraction: [0] = runtime_mutable, [1] = name, [2] = description
89
103
  └─────────────────────────────────────────────────────────────────────────────
90
104
  ┌─────────────────────────────────────────────────────────────────────────────
91
- 3-PARAMETER: property<T>(name, description, default)
92
- │ Example: property<int>(*this, "port", "Server port", 9092)
105
+ 4-PARAMETER: property<T>(runtime_mutable, name, description, default)
106
+ │ Example: property<int>(*this, false, "port", "Server port", 9092)
93
107
  │ Used for: Properties with simple custom default values
94
- │ Extraction: [0] = name, [1] = description, [2] = default
108
+ │ Extraction: [0] = runtime_mutable, [1] = name, [2] = description, [3] = default
95
109
  └─────────────────────────────────────────────────────────────────────────────
96
110
  ┌─────────────────────────────────────────────────────────────────────────────
97
- 4-PARAMETER: property<T>(name, description, meta, default)
98
- │ Example: property<bool>(*this, "flag", "Description", meta{.needs_restart=yes}, true)
111
+ 5-PARAMETER: property<T>(runtime_mutable, name, description, meta, default)
112
+ │ Example: property<bool>(*this, true, "flag", "Desc", meta{.needs_restart=yes}, true)
99
113
  │ Used for: Properties with metadata (restart requirements, visibility, etc.)
100
- │ Extraction: [0] = name, [1] = description, [2] = meta{}, [3] = default
114
+ │ Extraction: [0] = runtime_mutable, [1] = name, [2] = description, [3] = meta{}, [4] = default
115
+ └─────────────────────────────────────────────────────────────────────────────
116
+ ┌─────────────────────────────────────────────────────────────────────────────
117
+ │ WITH VALIDATOR: property<T>(runtime_mutable, name, description, [meta], default, validator)
118
+ │ Example: property<int>(*this, true, "port", "Port", 9092, [](int v){return v > 0;})
119
+ │ Used for: Properties with runtime validation functions
120
+ │ Extraction: Validator is automatically skipped; default is found by searching backwards
121
+ │ Note: Validators detected by is_validator_param() and excluded from default extraction
101
122
  └─────────────────────────────────────────────────────────────────────────────
102
123
 
103
124
  ENTERPRISE PROPERTY PATTERNS (More Complex):
@@ -517,6 +538,51 @@ def get_meta_value(info, key, default=None):
517
538
  return val
518
539
 
519
540
 
541
+ def resolve_cpp_literal(value_str):
542
+ """
543
+ Resolve C++ literal expressions to their actual values.
544
+
545
+ Handles:
546
+ - Size literals: 5_MiB → 5242880
547
+ - std::to_string() wrapper: std::to_string(5_MiB) → 5242880
548
+ - Plain integers: 42 → 42
549
+ - Plain floats: 3.14 → 3.14
550
+
551
+ Args:
552
+ value_str: String containing C++ literal expression
553
+
554
+ Returns:
555
+ Resolved value (int, float, or original string if can't resolve)
556
+ """
557
+ if not isinstance(value_str, str):
558
+ return value_str
559
+
560
+ val = value_str.strip()
561
+
562
+ # Handle std::to_string() wrapper
563
+ to_string_match = re.match(r"std::to_string\((.+)\)", val)
564
+ if to_string_match:
565
+ val = to_string_match.group(1).strip()
566
+
567
+ # Try size literals like 5_MiB
568
+ size_match = re.match(r"(\d+)_([KMGTP])iB", val)
569
+ if size_match:
570
+ num, unit = int(size_match.group(1)), size_match.group(2)
571
+ mult = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4, "P": 1024**5}[unit]
572
+ return num * mult
573
+
574
+ # Try plain integer
575
+ if re.fullmatch(r"-?\d+", val):
576
+ return int(val)
577
+
578
+ # Try plain float
579
+ if re.fullmatch(r"-?\d+\.\d+", val):
580
+ return float(val)
581
+
582
+ # Return original if can't resolve
583
+ return value_str
584
+
585
+
520
586
  def get_resolve_constexpr_identifier():
521
587
  """
522
588
  Lazily import constexpr identifier resolution function to avoid circular imports.
@@ -1391,31 +1457,37 @@ class SimpleDefaultValuesTransformer:
1391
1457
  return property
1392
1458
 
1393
1459
  # Find where the meta{} param is
1460
+ # MetaParamTransformer converts meta strings to dicts with "type": "initializer_list"
1394
1461
  meta_index = next(
1395
1462
  (i for i, p in enumerate(params)
1396
1463
  if isinstance(p.get("value"), (dict, str))
1397
1464
  and ("meta{" in str(p["value"]) or
1398
- (isinstance(p["value"], dict) and "needs_restart" in p["value"]))),
1399
- None,
1465
+ (isinstance(p["value"], dict) and p["value"].get("type") == "initializer_list"))
1466
+ ),
1467
+ None
1400
1468
  )
1401
1469
 
1402
- # Default comes immediately after meta
1403
- if meta_index is None:
1404
- default_index = 3 if len(params) > 3 else None
1405
- else:
1406
- default_index = meta_index + 1 if len(params) > meta_index + 1 else None
1470
+ # The default value is the LAST NON-VALIDATOR parameter in the constructor
1471
+ # Property structure: (*this, runtime_mutability_flag, name, description, [meta], default, [validator])
1472
+ # After *this is skipped: [0]=runtime_flag, [1]=name, [2]=desc, [3]=meta?, [4]=default, [5]=validator?
1473
+ # We need to search backwards to find the last parameter that's not a validator
1474
+ if len(params) < 2:
1475
+ return property
1407
1476
 
1408
- if default_index is None or default_index >= len(params):
1477
+ # Search backwards for the last non-validator parameter
1478
+ default_index = None
1479
+ for i in range(len(params) - 1, -1, -1):
1480
+ if not is_validator_param(params[i]):
1481
+ default_index = i
1482
+ break
1483
+
1484
+ if default_index is None:
1409
1485
  return property
1410
1486
 
1411
1487
  # Candidate default param
1412
1488
  default_param = params[default_index]
1413
1489
  default = default_param.get("value")
1414
1490
 
1415
- # Skip obvious validator params
1416
- if is_validator_param(default_param):
1417
- return property
1418
-
1419
1491
  # std::nullopt means "no default"
1420
1492
  if isinstance(default, str) and "std::nullopt" in default:
1421
1493
  property["default"] = None
@@ -1938,9 +2010,12 @@ class EnterpriseTransformer:
1938
2010
  info["params"] = params[:-1]
1939
2011
 
1940
2012
  # --- restricted_with_sanctioned (scalar form) ---
2013
+ # Pattern: (restricted, sanctioned, name, description, meta, default)
2014
+ # Must check that params[1] is NOT the property name (which would make it restricted_only)
1941
2015
  elif (
1942
2016
  len(params) >= 6
1943
2017
  and all(p["type"] in ("true", "false", "integer_literal", "string_literal", "qualified_identifier") for p in params[:2])
2018
+ and self._clean_value(params[1]["value"]) != info.get("name_in_file")
1944
2019
  ):
1945
2020
  enterprise_constructor = "restricted_with_sanctioned"
1946
2021
  restricted_vals = [self._clean_value(params[0]["value"])]
@@ -1955,13 +2030,15 @@ class EnterpriseTransformer:
1955
2030
  restricted_vals = [self._clean_value(params[0]["value"])]
1956
2031
  info["params"] = params[1:]
1957
2032
 
1958
- # --- simple enterprise property (lambda validator pattern) ---
1959
- elif (len(params) >= 3 and
1960
- params[0].get("type") == "lambda_expression" and
2033
+ # --- simple enterprise property (function/lambda validator pattern) ---
2034
+ # Pattern: (validator_function, name, description, meta, default, ...)
2035
+ # The validator can be a lambda or a function identifier
2036
+ elif (len(params) >= 3 and
2037
+ params[0].get("type") in ("lambda_expression", "identifier", "qualified_identifier") and
1961
2038
  params[1].get("type") == "string_literal"):
1962
2039
  enterprise_constructor = "simple"
1963
2040
  # Don't modify params for simple enterprise properties - they have normal structure
1964
- # Remove the lambda validator from parameters as it's not needed for documentation
2041
+ # Remove the validator from parameters as it's not needed for documentation
1965
2042
  info["params"] = params[1:]
1966
2043
 
1967
2044
  if not enterprise_constructor:
@@ -1980,28 +2057,6 @@ class EnterpriseTransformer:
1980
2057
  if sanctioned_vals is not None:
1981
2058
  property["enterprise_sanctioned_value"] = sanctioned_vals
1982
2059
 
1983
- # Add friendly description (values are already cleaned by _clean_value)
1984
- if enterprise_constructor == "restricted_with_sanctioned":
1985
- r = restricted_vals[0]
1986
- s = sanctioned_vals[0]
1987
- property["enterprise_default_description"] = (
1988
- f"Default: `{s}` (Community) or `{r}` (Enterprise)"
1989
- )
1990
- elif enterprise_constructor == "restricted_only":
1991
- if len(restricted_vals) > 1:
1992
- vals = ", ".join(f"`{v}`" for v in restricted_vals)
1993
- property["enterprise_default_description"] = (
1994
- f"Available only with Enterprise license: {vals}"
1995
- )
1996
- else:
1997
- property["enterprise_default_description"] = (
1998
- f"Available only with Enterprise license: `{restricted_vals[0]}`"
1999
- )
2000
- elif enterprise_constructor == "sanctioned_only":
2001
- property["enterprise_default_description"] = (
2002
- f"Community-only configuration. Sanctioned value: `{sanctioned_vals[0]}`"
2003
- )
2004
-
2005
2060
  return property
2006
2061
 
2007
2062
  # --- Helper: clean literal/identifier values ---
@@ -2107,6 +2162,8 @@ class MetaParamTransformer:
2107
2162
 
2108
2163
  key, value = [s.strip() for s in field.split("=", 1)]
2109
2164
  clean_key = key.replace(".", "")
2165
+ # Strip trailing commas from value (last field in meta block has trailing comma)
2166
+ value = value.rstrip(",").strip()
2110
2167
  meta_dict[clean_key] = value
2111
2168
 
2112
2169
  # 🔹 Inline special handlers for known meta keys
@@ -2115,13 +2172,12 @@ class MetaParamTransformer:
2115
2172
  # Example values
2116
2173
  if clean_key == "example":
2117
2174
  val_clean = value.strip().strip('"')
2118
- # Try to coerce to int or float, else leave as string
2119
- if re.fullmatch(r"-?\d+", val_clean):
2120
- property["example"] = int(val_clean)
2121
- elif re.fullmatch(r"-?\d+\.\d+", val_clean):
2122
- property["example"] = float(val_clean)
2175
+ resolved = resolve_cpp_literal(val_clean)
2176
+ # Wrap in backticks for inline code formatting (handles overrides with AsciiDoc blocks)
2177
+ if isinstance(resolved, str):
2178
+ property["example"] = f"`{normalize_string(resolved)}`"
2123
2179
  else:
2124
- property["example"] = normalize_string(val_clean)
2180
+ property["example"] = f"`{resolved}`"
2125
2181
 
2126
2182
  # Gets_restored / restored flags
2127
2183
  elif clean_key in ("gets_restored", "restored"):
@@ -2204,21 +2260,18 @@ class ExampleTransformer:
2204
2260
  meta_dict = param["value"]
2205
2261
  if "example" in meta_dict:
2206
2262
  example_val = meta_dict["example"]
2207
-
2263
+
2208
2264
  # Clean up the value (remove quotes, etc.)
2209
2265
  if isinstance(example_val, str):
2210
2266
  example_val = example_val.strip().strip('"\'')
2211
-
2212
- # Try to coerce to appropriate type
2213
- if isinstance(example_val, str):
2214
- if re.fullmatch(r"-?\d+", example_val):
2215
- property["example"] = int(example_val)
2216
- elif re.fullmatch(r"-?\d+\.\d+", example_val):
2217
- property["example"] = float(example_val)
2218
- else:
2219
- property["example"] = example_val
2267
+
2268
+ # Use shared resolution logic
2269
+ resolved = resolve_cpp_literal(example_val)
2270
+ # Wrap in backticks for inline code formatting
2271
+ if isinstance(resolved, str):
2272
+ property["example"] = f"`{normalize_string(resolved)}`"
2220
2273
  else:
2221
- property["example"] = example_val
2274
+ property["example"] = f"`{resolved}`"
2222
2275
  break
2223
2276
 
2224
2277
  return property
@@ -1,173 +0,0 @@
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