@redpanda-data/docs-extensions-and-macros 4.10.0 → 4.10.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 CHANGED
@@ -1111,6 +1111,8 @@ automation
1111
1111
  } else {
1112
1112
  env.OUTPUT_JSON_DIR = path.resolve(outputDir, 'examples');
1113
1113
  env.OUTPUT_AUTOGENERATED_DIR = path.resolve(outputDir);
1114
+ // Set property files to go to properties subdirectory
1115
+ env.OUTPUT_ASCIIDOC_DIR = path.resolve(outputDir, 'pages', 'properties');
1114
1116
  }
1115
1117
 
1116
1118
  const r = spawnSync('make', args, { cwd, stdio: 'inherit', env });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.10.0",
3
+ "version": "4.10.1",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -111,10 +111,10 @@ generate-docs: node-deps
111
111
  @# Use the enhanced properties file (with overrides) for documentation generation if it exists
112
112
  @if [ -f "$(TOOL_ROOT)/gen/$(TAG)-properties.json" ]; then \
113
113
  cd $(TOOL_ROOT) && \
114
- node generate-handlebars-docs.js "gen/$(TAG)-properties.json" "$(OUTPUT_AUTOGENERATED_DIR)"; \
114
+ node generate-handlebars-docs.js "gen/$(TAG)-properties.json" "$(OUTPUT_ASCIIDOC_DIR)"; \
115
115
  else \
116
116
  cd $(TOOL_ROOT) && \
117
- node generate-handlebars-docs.js "gen/properties-output.json" "$(OUTPUT_AUTOGENERATED_DIR)"; \
117
+ node generate-handlebars-docs.js "gen/properties-output.json" "$(OUTPUT_ASCIIDOC_DIR)"; \
118
118
  fi
119
119
  @echo "📄 Copying properties JSON files to $(OUTPUT_JSON_DIR)…"
120
120
  @if [ -f "$(TOOL_ROOT)/gen/$(TAG)-properties.json" ]; then \
@@ -60,7 +60,17 @@ NOTE: Some cluster properties require that you restart the cluster for any updat
60
60
  sectionTitle: 'Cluster configuration',
61
61
  groups: [
62
62
  {
63
- filter: (prop) => prop.config_scope === 'cluster' && !prop.is_deprecated
63
+ filter: (prop) => prop.config_scope === 'cluster' && !prop.is_deprecated && !(
64
+ prop.name && (
65
+ prop.name.includes('cloud_storage') ||
66
+ prop.name.includes('s3_') ||
67
+ prop.name.includes('azure_') ||
68
+ prop.name.includes('gcs_') ||
69
+ prop.name.includes('archival_') ||
70
+ prop.name.includes('remote_') ||
71
+ prop.name.includes('tiered_')
72
+ )
73
+ )
64
74
  }
65
75
  ],
66
76
  filename: 'cluster-properties.adoc'
@@ -269,7 +279,8 @@ function generateDeprecatedDocs(properties, outputDir) {
269
279
  };
270
280
 
271
281
  const output = template(data);
272
- const outputPath = path.join(outputDir, 'deprecated', 'partials', 'deprecated-properties.adoc');
282
+ // Navigate back from pages/properties to reference, then into partials/deprecated
283
+ const outputPath = path.join(path.dirname(path.dirname(outputDir)), 'partials', 'deprecated', 'deprecated-properties.adoc');
273
284
 
274
285
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
275
286
  fs.writeFileSync(outputPath, output, 'utf8');
@@ -332,7 +343,7 @@ function generateAllDocs(inputFile, outputDir) {
332
343
 
333
344
  // Generate each type of documentation
334
345
  for (const [type, config] of Object.entries(PROPERTY_CONFIG)) {
335
- const count = generatePropertyDocs(properties, config, path.join(outputDir, 'pages'));
346
+ const count = generatePropertyDocs(properties, config, outputDir);
336
347
  totalProperties += count;
337
348
 
338
349
  if (type === 'broker') totalBrokerProperties = count;
@@ -342,7 +353,7 @@ function generateAllDocs(inputFile, outputDir) {
342
353
  }
343
354
 
344
355
  // Generate deprecated properties documentation
345
- const deprecatedCount = generateDeprecatedDocs(properties, path.join(outputDir, 'pages'));
356
+ const deprecatedCount = generateDeprecatedDocs(properties, outputDir);
346
357
 
347
358
  // Generate summary file
348
359
  const allPropertiesContent = Object.keys(properties).sort().join('\n');
@@ -117,6 +117,24 @@ def process_enterprise_value(enterprise_str):
117
117
  """
118
118
  enterprise_str = enterprise_str.strip()
119
119
 
120
+ # Handle special SASL mechanism function names
121
+ if enterprise_str == "is_enterprise_sasl_mechanism":
122
+ # Dynamically look up enterprise SASL mechanisms from source
123
+ enterprise_mechanisms = get_enterprise_sasl_mechanisms()
124
+ if enterprise_mechanisms:
125
+ return enterprise_mechanisms
126
+ else:
127
+ # Fallback to known values if lookup fails
128
+ return ["GSSAPI", "OAUTHBEARER"]
129
+ elif enterprise_str == "is_enterprise_sasl_mechanisms_override":
130
+ # Get the enterprise mechanisms dynamically for a more accurate description
131
+ enterprise_mechanisms = get_enterprise_sasl_mechanisms()
132
+ if enterprise_mechanisms:
133
+ mechanism_list = ", ".join(enterprise_mechanisms)
134
+ return f"Any override containing enterprise mechanisms ({mechanism_list})."
135
+ else:
136
+ return "Any override containing enterprise mechanisms."
137
+
120
138
  # FIRST: Handle std::vector initialization patterns (highest priority)
121
139
  # This must come before enum processing because vectors can contain enums
122
140
  # Tolerate optional whitespace around braces
@@ -196,19 +214,15 @@ def process_enterprise_value(enterprise_str):
196
214
 
197
215
  def resolve_cpp_function_call(function_name):
198
216
  """
199
- Resolve certain small, known C++ zero-argument functions to their literal return values by searching Redpanda source files.
217
+ Resolve a small set of known zero-argument C++ functions to their literal string return values by scanning a local Redpanda source tree.
200
218
 
201
- This function looks up predefined search patterns for well-known functions (currently a small set under `model::*`), locates a local Redpanda source tree from several commonly used paths, and scans the listed files (and, if needed, the broader model directory) for a regex match that captures the string returned by the function. If a match is found the captured string is returned; if the source tree cannot be found or no match is located the function returns None.
219
+ Searches predefined files and regex patterns for the specified fully-qualified function name (e.g., "model::kafka_audit_logging_topic") and returns the captured string if found; returns None when no match or when the Redpanda source tree cannot be located.
202
220
 
203
221
  Parameters:
204
- function_name (str): Fully-qualified C++ function name to resolve (e.g., "model::kafka_audit_logging_topic").
222
+ function_name (str): Fully-qualified C++ function name to resolve.
205
223
 
206
224
  Returns:
207
- str or None: The resolved literal string returned by the C++ function, or None when unresolved (source not found or no matching pattern).
208
-
209
- Notes:
210
- - The function performs filesystem I/O and regex-based source searching; it does not raise on read errors but logs and continues.
211
- - Only a small, hard-coded set of function names/patterns is supported; unknown names immediately return None.
225
+ str or None: The literal string returned by the C++ function when resolved, or `None` if unresolved.
212
226
  """
213
227
  # Map function names to likely search patterns and file locations
214
228
  search_patterns = {
@@ -322,7 +336,174 @@ def resolve_cpp_function_call(function_name):
322
336
  return None
323
337
 
324
338
 
339
+ def resolve_constexpr_identifier(identifier):
340
+ """
341
+ Resolve a constexpr identifier from Redpanda source code to its literal string value.
342
+
343
+ Searches common Redpanda source locations for constexpr string or string_view definitions matching the given identifier and returns the literal if found.
344
+
345
+ Parameters:
346
+ identifier (str): The identifier name to resolve (e.g., "scram").
347
+
348
+ Returns:
349
+ str or None: The resolved literal string value if found, otherwise `None`.
350
+ """
351
+ # Try to find the Redpanda source directory
352
+ redpanda_source_paths = [
353
+ 'tmp/redpanda', # Current directory
354
+ '../tmp/redpanda', # Parent directory
355
+ 'tools/property-extractor/tmp/redpanda', # From project root
356
+ os.path.join(os.getcwd(), 'tools', 'property-extractor', 'tmp', 'redpanda')
357
+ ]
358
+
359
+ redpanda_source = None
360
+ for path in redpanda_source_paths:
361
+ if os.path.exists(path):
362
+ redpanda_source = path
363
+ break
364
+
365
+ if not redpanda_source:
366
+ logger.debug(f"Could not find Redpanda source directory to resolve identifier: {identifier}")
367
+ return None
368
+
369
+ # Pattern to match constexpr string_view definitions
370
+ # Matches: inline constexpr std::string_view scram{"SCRAM"};
371
+ patterns = [
372
+ rf'inline\s+constexpr\s+std::string_view\s+{re.escape(identifier)}\s*\{{\s*"([^"]+)"\s*\}}',
373
+ rf'constexpr\s+std::string_view\s+{re.escape(identifier)}\s*\{{\s*"([^"]+)"\s*\}}',
374
+ rf'inline\s+constexpr\s+auto\s+{re.escape(identifier)}\s*=\s*"([^"]+)"',
375
+ rf'constexpr\s+auto\s+{re.escape(identifier)}\s*=\s*"([^"]+)"',
376
+ rf'static\s+constexpr\s+std::string_view\s+{re.escape(identifier)}\s*\{{\s*"([^"]+)"\s*\}}',
377
+ rf'static\s+inline\s+constexpr\s+std::string_view\s+{re.escape(identifier)}\s*\{{\s*"([^"]+)"\s*\}}',
378
+ ]
379
+
380
+ # Search recursively through the config directory and other common locations
381
+ search_dirs = [
382
+ os.path.join(redpanda_source, 'src', 'v', 'config'),
383
+ os.path.join(redpanda_source, 'src', 'v', 'kafka'),
384
+ os.path.join(redpanda_source, 'src', 'v', 'security'),
385
+ os.path.join(redpanda_source, 'src', 'v', 'pandaproxy'),
386
+ ]
387
+
388
+ for search_dir in search_dirs:
389
+ if not os.path.exists(search_dir):
390
+ continue
391
+
392
+ # Walk through the directory recursively
393
+ for root, dirs, files in os.walk(search_dir):
394
+ for file in files:
395
+ # Check both .h and .cc files since definitions can be in either
396
+ if file.endswith(('.h', '.cc', '.hpp', '.cpp')):
397
+ file_path = os.path.join(root, file)
398
+ try:
399
+ with open(file_path, 'r', encoding='utf-8') as f:
400
+ content = f.read()
401
+
402
+ # Try each pattern
403
+ for pattern in patterns:
404
+ match = re.search(pattern, content, re.MULTILINE)
405
+ if match:
406
+ resolved_value = match.group(1)
407
+ logger.debug(f"Resolved identifier '{identifier}' -> '{resolved_value}' from {file_path}")
408
+ return resolved_value
409
+
410
+ except (FileNotFoundError, PermissionError, OSError, UnicodeDecodeError) as e:
411
+ logger.debug(f"Error reading {file_path}: {e}")
412
+ continue
413
+
414
+ logger.debug(f"Could not resolve identifier: {identifier}")
415
+ return None
416
+
417
+
418
+ def get_enterprise_sasl_mechanisms():
419
+ """
420
+ Locate and resolve enterprise SASL mechanisms declared in Redpanda's sasl_mechanisms.h.
421
+
422
+ Searches known Redpanda source locations for an inline constexpr definition of enterprise_sasl_mechanisms,
423
+ extracts the identifiers, and resolves each identifier to its literal string value where possible; unresolved
424
+ identifiers are converted to an uppercase fallback.
425
+
426
+ Returns:
427
+ list or None: List of enterprise SASL mechanism strings (e.g., ["GSSAPI", "OAUTHBEARER"]),
428
+ or `None` if the lookup fails.
429
+ """
430
+ # Try to find the Redpanda source directory
431
+ redpanda_source_paths = [
432
+ 'tmp/redpanda', # Current directory
433
+ '../tmp/redpanda', # Parent directory
434
+ 'tools/property-extractor/tmp/redpanda', # From project root
435
+ os.path.join(os.getcwd(), 'tools', 'property-extractor', 'tmp', 'redpanda')
436
+ ]
437
+
438
+ redpanda_source = None
439
+ for path in redpanda_source_paths:
440
+ if os.path.exists(path):
441
+ redpanda_source = path
442
+ break
443
+
444
+ if not redpanda_source:
445
+ logger.debug("Could not find Redpanda source directory to resolve enterprise SASL mechanisms")
446
+ return None
447
+
448
+ # Look for the enterprise_sasl_mechanisms definition in sasl_mechanisms.h
449
+ sasl_mechanisms_file = os.path.join(redpanda_source, 'src', 'v', 'config', 'sasl_mechanisms.h')
450
+
451
+ if not os.path.exists(sasl_mechanisms_file):
452
+ logger.debug(f"sasl_mechanisms.h not found at {sasl_mechanisms_file}")
453
+ return None
454
+
455
+ try:
456
+ with open(sasl_mechanisms_file, 'r', encoding='utf-8') as f:
457
+ content = f.read()
458
+
459
+ # Pattern to match the enterprise_sasl_mechanisms array definition
460
+ # inline constexpr auto enterprise_sasl_mechanisms = std::to_array<std::string_view>({gssapi, oauthbearer});
461
+ pattern = r'inline\s+constexpr\s+auto\s+enterprise_sasl_mechanisms\s*=\s*std::to_array<[^>]+>\s*\(\s*\{\s*([^}]+)\s*\}\s*\)'
462
+
463
+ match = re.search(pattern, content, re.MULTILINE | re.DOTALL)
464
+ if match:
465
+ # Extract the identifiers from the array (e.g., "gssapi, oauthbearer")
466
+ identifiers_str = match.group(1).strip()
467
+
468
+ # Split by comma and clean up whitespace
469
+ identifiers = [id.strip() for id in identifiers_str.split(',') if id.strip()]
470
+
471
+ # Resolve each identifier to its actual string value
472
+ mechanisms = []
473
+ for identifier in identifiers:
474
+ resolved_value = resolve_constexpr_identifier(identifier)
475
+ if resolved_value:
476
+ mechanisms.append(resolved_value)
477
+ else:
478
+ logger.debug(f"Could not resolve SASL mechanism identifier: {identifier}")
479
+ # Fallback: use the identifier name in uppercase
480
+ mechanisms.append(identifier.upper())
481
+
482
+ if mechanisms:
483
+ logger.debug(f"Resolved enterprise SASL mechanisms: {mechanisms}")
484
+ return mechanisms
485
+ else:
486
+ logger.debug("Could not find enterprise_sasl_mechanisms definition in sasl_mechanisms.h")
487
+ return None
488
+
489
+ except (OSError, UnicodeDecodeError, re.error) as e:
490
+ logger.debug(f"Error reading {sasl_mechanisms_file}: {e}")
491
+ return None
492
+
493
+
325
494
  def validate_paths(options):
495
+ """
496
+ Validate that required file-system paths referenced by `options` exist and exit the process on failure.
497
+
498
+ Checks:
499
+ - Verifies `options.path` exists; logs an error and exits with status code 1 if it does not.
500
+ - If `options.definitions` is provided, verifies that file exists; logs an error and exits with status code 1 if it does not.
501
+
502
+ Parameters:
503
+ options: An object with at least the attributes:
504
+ - path (str): Path to the input source directory or file.
505
+ - definitions (Optional[str]): Path to the type definitions file (may be None or empty).
506
+ """
326
507
  path = options.path
327
508
 
328
509
  if not os.path.exists(path):
@@ -713,21 +894,16 @@ def add_config_scope(properties):
713
894
 
714
895
  def resolve_type_and_default(properties, definitions):
715
896
  """
716
- Resolve JSON Schema types and expand C++-style default values for all properties.
897
+ Normalize property types and expand C++-style default values into JSON-compatible Python structures.
717
898
 
718
- This function:
719
- - Resolves type references found in `properties` against `definitions` (supports "$ref" and direct type names) and normalizes property "type" to a JSON Schema primitive ("object", "string", "integer", "boolean", "array", "number") with sensible fallbacks.
720
- - Expands C++ constructor/initializer syntax and common C++ patterns appearing in default values into JSON-compatible Python values (e.g., nested constructor calls -> dicts, initializer lists -> lists, `std::nullopt` -> None, enum-like tokens -> strings).
721
- - Ensures array-typed properties (including one_or_many_property cases) have array defaults: single-object defaults are wrapped into a one-element list and "{}" string defaults become [].
722
- - Updates array item type information when item types reference definitions.
723
- - Applies a final pass to convert any remaining C++-patterned defaults and to transform any `enterprise_value` strings via process_enterprise_value.
899
+ This function resolves type references in each property against the provided definitions (supports "$ref" and direct type names), normalizes property "type" to a JSON Schema primitive when possible, expands C++ constructor/initializer and common C++ literal patterns found in "default" values into Python primitives/objects/lists, ensures array-typed properties have array defaults (including handling one_or_many_property cases), updates array item type information when item types reference definitions, and converts any `enterprise_value` strings via process_enterprise_value.
724
900
 
725
901
  Parameters:
726
- properties (dict): Mapping of property names to property metadata dictionaries. Each property may include keys like "type", "default", "items", and "enterprise_value".
727
- definitions (dict): Mapping of type names to JSON Schema definition dictionaries used to resolve $ref targets and to infer property shapes when expanding constructors.
902
+ properties (dict): Mapping of property names to metadata dictionaries. Relevant keys that may be modified include "type", "default", "items", and "enterprise_value".
903
+ definitions (dict): Mapping of definition names to JSON Schema definition dictionaries used to resolve $ref targets and to infer shapes for expanding constructor-style defaults.
728
904
 
729
905
  Returns:
730
- dict: The same `properties` mapping after in-place normalization and expansion of types and defaults.
906
+ dict: The same `properties` mapping after in-place normalization and expansion of types, defaults, item types, and enterprise values.
731
907
  """
732
908
  import ast
733
909
  import re
@@ -742,20 +918,42 @@ def resolve_type_and_default(properties, definitions):
742
918
  return defn
743
919
 
744
920
  def parse_constructor(s):
745
- """Parse C++ constructor syntax into type name and arguments."""
921
+ """
922
+ Parse a C++-style constructor or initializer expression into its type name and argument list.
923
+
924
+ Parses input forms such as `Type(arg1, arg2)`, `Type{arg1, arg2}`, or plain literals/enum-like tokens. For string literals the returned argument is a Python string value; for integer literals the returned argument is an int. Nested constructors and nested brace/paren groups are preserved as argument tokens.
925
+
926
+ Parameters:
927
+ s (str): The C++ expression to parse.
928
+
929
+ Returns:
930
+ tuple:
931
+ - type_name (str|None): The parsed type name for constructor forms, or `None` when `s` is a primitive literal or enum-like token.
932
+ - args (list): A list of argument tokens; tokens are raw strings for complex/nested arguments, Python `str` for quoted string literals, or `int` for integer literals.
933
+ """
746
934
  s = s.strip()
935
+ original_s = s
747
936
  if s.startswith("{") and s.endswith("}"):
748
937
  s = s[1:-1].strip()
938
+
939
+ # Try parentheses syntax first: type_name(args)
749
940
  match = re.match(r'([a-zA-Z0-9_:]+)\((.*)\)', s)
750
- if not match:
751
- # Primitive or enum
752
- if s.startswith('"') and s.endswith('"'):
753
- return None, [ast.literal_eval(s)]
754
- try:
755
- return None, [int(s)]
756
- except Exception:
757
- return None, [s]
758
- type_name, arg_str = match.groups()
941
+ if match:
942
+ type_name, arg_str = match.groups()
943
+ else:
944
+ # Try curly brace syntax: type_name{args}
945
+ match = re.match(r'([a-zA-Z0-9_:]+)\{(.*)\}', s)
946
+ if match:
947
+ type_name, arg_str = match.groups()
948
+ else:
949
+ # Primitive or enum
950
+ if s.startswith('"') and s.endswith('"'):
951
+ return None, [ast.literal_eval(s)]
952
+ try:
953
+ return None, [int(s)]
954
+ except ValueError:
955
+ return None, [s]
956
+
759
957
  args = []
760
958
  depth = 0
761
959
  current = ''
@@ -768,9 +966,9 @@ def resolve_type_and_default(properties, definitions):
768
966
  args.append(current.strip())
769
967
  current = ''
770
968
  else:
771
- if c == '(' and not in_string:
969
+ if c in '({' and not in_string:
772
970
  depth += 1
773
- elif c == ')' and not in_string:
971
+ elif c in ')}' and not in_string:
774
972
  depth -= 1
775
973
  current += c
776
974
  if current.strip():
@@ -779,13 +977,18 @@ def resolve_type_and_default(properties, definitions):
779
977
 
780
978
  def process_cpp_patterns(arg_str):
781
979
  """
782
- Process specific C++ patterns to user-friendly values.
980
+ Convert a C++-style expression string into a JSON-friendly literal representation.
783
981
 
784
- Handles:
785
- - net::unresolved_address("127.0.0.1", 9092) -> expands based on type definition
982
+ This function recognises common C++ patterns produced by the extractor and maps them to values suitable for JSON schema defaults and examples. Handled cases include:
786
983
  - std::nullopt -> null
787
- - fips_mode_flag::disabled -> "disabled"
788
- - model::kafka_audit_logging_topic() -> dynamically looked up from source
984
+ - zero-argument functions (e.g., model::kafka_audit_logging_topic()) resolved from source when possible
985
+ - enum tokens (e.g., fips_mode_flag::disabled -> "disabled")
986
+ - constexpr identifiers and simple string constructors resolved to their literal strings when available
987
+ - known default constructors and truncated type names mapped to sensible defaults (e.g., duration -> 0, path -> "")
988
+ - simple heuristics for unknown constructors and concatenated expressions
989
+
990
+ Returns:
991
+ processed (str): A string representing the JSON-ready value (for example: '"value"', 'null', '0', or the original input when no mapping applied).
789
992
  """
790
993
  arg_str = arg_str.strip()
791
994
 
@@ -808,6 +1011,24 @@ def resolve_type_and_default(properties, definitions):
808
1011
  enum_value = enum_match.group(1)
809
1012
  return f'"{enum_value}"'
810
1013
 
1014
+ # Handle constexpr identifier resolution (such as scram -> "SCRAM")
1015
+ # Check if this is a simple identifier that might be a constexpr variable
1016
+ if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg_str):
1017
+ resolved_value = resolve_constexpr_identifier(arg_str)
1018
+ if resolved_value is not None:
1019
+ return f'"{resolved_value}"'
1020
+
1021
+ # Handle string constructor patterns like ss::sstring{identifier}
1022
+ sstring_match = re.match(r'ss::sstring\{([a-zA-Z_][a-zA-Z0-9_]*)\}', arg_str)
1023
+ if sstring_match:
1024
+ identifier = sstring_match.group(1)
1025
+ resolved_value = resolve_constexpr_identifier(identifier)
1026
+ if resolved_value is not None:
1027
+ return f'"{resolved_value}"'
1028
+ else:
1029
+ # Fallback to the identifier itself
1030
+ return f'"{identifier}"'
1031
+
811
1032
  # Handle default constructors and their default values
812
1033
  # This handles cases where C++ default constructors are used but should map to specific values
813
1034
 
@@ -861,12 +1082,23 @@ def resolve_type_and_default(properties, definitions):
861
1082
 
862
1083
  def expand_default(type_name, default_str):
863
1084
  """
864
- Expand C++ default values into structured JSON objects.
1085
+ Convert a C++-style default initializer into a JSON-serializable Python value.
865
1086
 
866
- For array types with initializer list syntax like:
867
- {model::broker_endpoint(net::unresolved_address("127.0.0.1", 9644))}
1087
+ This expands C++ constructor and initializer-list syntax into Python primitives, dictionaries, and lists suitable for JSON output. Supported transformations include:
1088
+ - String constructors and quoted literals → Python str.
1089
+ - Integer and boolean literals → Python int and bool.
1090
+ - Object constructors (Type(arg1, arg2) or Type{...}) → dict mapping constructor arguments to the object's properties when a corresponding type definition exists.
1091
+ - Nested constructors → nested dicts with their fields expanded.
1092
+ - Array initializer lists (e.g., {Type(...), Type(...)}) → Python list with each element expanded.
1093
+ - Special-case mappings for known type patterns (for example, an address-type constructor expanded into {"address", "port"} when the target type expects that shape).
1094
+ If a default cannot be resolved or the type is an enum, the original input is returned unchanged; the string "null" is converted to None. If default_str is not a string, it is returned as-is.
868
1095
 
869
- This creates: [{address: "127.0.0.1", port: 9644}]
1096
+ Parameters:
1097
+ type_name (str): The resolved type name for the default value (e.g., "model::broker_endpoint" or a primitive type like "string").
1098
+ default_str (str | any): The C++ default expression to expand, or a non-string value already decoded.
1099
+
1100
+ Returns:
1101
+ The expanded Python representation of the default: a dict for objects, a list for arrays, a primitive (str/int/bool), None for null, or the original value/string when expansion is not possible.
870
1102
  """
871
1103
  # Handle non-string defaults
872
1104
  if not isinstance(default_str, str):
@@ -883,6 +1115,19 @@ def resolve_type_and_default(properties, definitions):
883
1115
  return ast.literal_eval(processed)
884
1116
  else:
885
1117
  return processed
1118
+
1119
+ # Handle string type with constructor syntax (e.g., ss::sstring{scram})
1120
+ if type_name == "string" and ("{" in default_str or "(" in default_str):
1121
+ tname, args = parse_constructor(default_str)
1122
+ if tname and args:
1123
+ # For string constructors, resolve the first argument and return it as the string value
1124
+ first_arg = args[0] if args else ""
1125
+ # Apply C++ pattern processing to resolve identifiers
1126
+ processed_arg = process_cpp_patterns(first_arg)
1127
+ if processed_arg.startswith('"') and processed_arg.endswith('"'):
1128
+ return ast.literal_eval(processed_arg) # Remove quotes
1129
+ else:
1130
+ return processed_arg
886
1131
 
887
1132
  type_def = resolve_definition_type(definitions.get(type_name, {}))
888
1133
  if "enum" in type_def:
@@ -1619,4 +1864,4 @@ def main():
1619
1864
  sys.exit(1)
1620
1865
 
1621
1866
  if __name__ == "__main__":
1622
- main()
1867
+ main()
@@ -12,12 +12,12 @@ logger = logging.getLogger(__name__)
12
12
  # the centralized enterprise value processing logic without creating import cycles.
13
13
  def get_process_enterprise_value():
14
14
  """
15
- Lazily import and return the centralized `process_enterprise_value` function from `property_extractor`.
15
+ Lazily load the centralized process_enterprise_value function from property_extractor.
16
16
 
17
- Attempts to import `process_enterprise_value` and return it to avoid circular-import issues. If the import fails an error message is printed and None is returned.
17
+ Attempts to import and return the `process_enterprise_value` callable; logs an error and returns `None` if the import fails.
18
18
 
19
19
  Returns:
20
- Callable or None: The `process_enterprise_value` callable when available, otherwise `None`.
20
+ The `process_enterprise_value` callable if available, `None` otherwise.
21
21
  """
22
22
  try:
23
23
  from property_extractor import process_enterprise_value
@@ -27,6 +27,23 @@ def get_process_enterprise_value():
27
27
  return None
28
28
 
29
29
 
30
+ def get_resolve_constexpr_identifier():
31
+ """
32
+ Lazily import and return the `resolve_constexpr_identifier` function from `property_extractor`.
33
+
34
+ Attempts to import `resolve_constexpr_identifier` and return it to avoid circular-import issues.
35
+
36
+ Returns:
37
+ Callable or None: The `resolve_constexpr_identifier` callable when available, otherwise `None`.
38
+ """
39
+ try:
40
+ from property_extractor import resolve_constexpr_identifier
41
+ return resolve_constexpr_identifier
42
+ except ImportError as e:
43
+ logger.exception("Cannot import resolve_constexpr_identifier from property_extractor: %s", e)
44
+ return None
45
+
46
+
30
47
  class BasicInfoTransformer:
31
48
  def accepts(self, info, file_pair):
32
49
  """
@@ -182,29 +199,59 @@ class TypeTransformer:
182
199
 
183
200
  def get_cpp_type_from_declaration(self, declaration):
184
201
  """
185
- Extract the inner type from C++ property declarations.
186
-
187
- This method handles various C++ template types and extracts the core type T from:
188
- - property<T> -> T
189
- - std::optional<T> -> T
190
- - std::vector<T> -> T
191
- - one_or_many_property<T> -> T (Redpanda's flexible array type)
202
+ Extract the inner C++ type from wrapped declarations like `property<T>`, `std::optional<T>`, `std::vector<T>`, or `one_or_many_property<T>`.
192
203
 
193
- For one_or_many_property, this is crucial because it allows the same property
194
- to accept either a single value or an array of values in the configuration.
195
- Examples:
196
- - one_or_many_property<model::broker_endpoint> -> model::broker_endpoint
197
- - one_or_many_property<endpoint_tls_config> -> endpoint_tls_config
204
+ Parses common wrapper templates and returns the unwrapped type name (for example, returns `model::broker_endpoint` from `one_or_many_property<model::broker_endpoint>`). The returned type is intended for downstream mapping to JSON schema types and default value resolution.
198
205
 
199
- The extracted type is then used to determine the JSON schema type and
200
- for resolving default values from the definitions.
206
+ Returns:
207
+ raw_type (str): The extracted inner C++ type as a string, or a best-effort fragment of the declaration if a precise extraction cannot be performed.
201
208
  """
202
209
  one_line_declaration = declaration.replace("\n", "").strip()
203
- raw_type = (
204
- re.sub(r"^.*property<(.+)>.*", "\\1", one_line_declaration)
205
- .split()[0]
206
- .replace(",", "")
207
- )
210
+
211
+ # Extract property template content with proper nesting handling
212
+ # This handles cases like property<std::vector<config::sasl_mechanisms_override>>
213
+ def extract_template_content(text, template_name):
214
+ """
215
+ Extracts the inner contents of the first occurrence of a template with the given name, correctly handling nested angle brackets.
216
+
217
+ Parameters:
218
+ text (str): The string to search for the template.
219
+ template_name (str): The template name (e.g., "std::vector" or "property").
220
+
221
+ Returns:
222
+ str or None: The substring inside the outermost angle brackets for the matched template (excluding the brackets),
223
+ or `None` if the template is not found or angle brackets are unbalanced.
224
+ """
225
+ start_idx = text.find(f'{template_name}<')
226
+ if start_idx == -1:
227
+ return None
228
+
229
+ start_idx += len(f'{template_name}<')
230
+ bracket_count = 1
231
+ i = start_idx
232
+
233
+ while i < len(text) and bracket_count > 0:
234
+ if text[i] == '<':
235
+ bracket_count += 1
236
+ elif text[i] == '>':
237
+ bracket_count -= 1
238
+ i += 1
239
+
240
+ if bracket_count == 0:
241
+ return text[start_idx:i-1]
242
+ return None
243
+
244
+ # Extract the content from property<...>
245
+ property_content = extract_template_content(one_line_declaration, 'property')
246
+ if property_content:
247
+ raw_type = property_content.split()[0].replace(",", "")
248
+ else:
249
+ # Fallback to original regex for simpler cases
250
+ raw_type = (
251
+ re.sub(r"^.*property<(.+)>.*", "\\1", one_line_declaration)
252
+ .split()[0]
253
+ .replace(",", "")
254
+ )
208
255
 
209
256
  if self.OPTIONAL_PATTERN in raw_type:
210
257
  raw_type = re.sub(".*std::optional<(.+)>.*", "\\1", raw_type)
@@ -223,6 +270,15 @@ class TypeTransformer:
223
270
  return raw_type
224
271
 
225
272
  def get_type_from_declaration(self, declaration):
273
+ """
274
+ Map a C++ type declaration string to a simplified, user-facing type name.
275
+
276
+ Parameters:
277
+ declaration (str): C++ type declaration or template expression from which the effective type will be derived.
278
+
279
+ Returns:
280
+ str: A JSON-schema-friendly type name such as "integer", "number", "string", "string[]", or "boolean". If no mapping matches, returns the normalized/raw extracted C++ type.
281
+ """
226
282
  raw_type = self.get_cpp_type_from_declaration(declaration)
227
283
  type_mapping = [ # (regex, type)
228
284
  ("^u(nsigned|int)", "integer"),
@@ -240,9 +296,29 @@ class TypeTransformer:
240
296
  if re.search(m[0], raw_type):
241
297
  return m[1]
242
298
 
299
+ # Handle specific user-unfriendly C++ types with descriptive alternatives
300
+ # Map complex C++ config types to user-friendly JSON schema types
301
+ user_friendly_types = {
302
+ "config::sasl_mechanisms_override": "object",
303
+ }
304
+
305
+ if raw_type in user_friendly_types:
306
+ return user_friendly_types[raw_type]
307
+
243
308
  return raw_type
244
309
 
245
310
  def parse(self, property, info, file_pair):
311
+ """
312
+ Set the property's "type" field to the JSON schema type derived from the C++ declaration.
313
+
314
+ Parameters:
315
+ property (dict): Mutable property bag to be updated.
316
+ info (dict): Parsed property metadata; its "declaration" field is used to determine the type.
317
+ file_pair: Unused here; present for transformer interface compatibility.
318
+
319
+ Returns:
320
+ property (dict): The same property bag with "type" set to the derived type string.
321
+ """
246
322
  property["type"] = self.get_type_from_declaration(info["declaration"])
247
323
  return property
248
324
 
@@ -394,42 +470,170 @@ class FriendlyDefaultTransformer:
394
470
 
395
471
  # Class-level constants for pattern matching in default values
396
472
  ARRAY_PATTERN_STD_VECTOR = "std::vector"
473
+ SSTRING_CONSTRUCTOR_PATTERN = r'ss::sstring\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
474
+ VECTOR_INITIALIZER_PATTERN = r'std::vector<[^>]+>\s*\{(.*)\}$'
475
+ CHRONO_PATTERN = r"std::chrono::(\w+)\(([^)]+)\)"
476
+
477
+ def __init__(self):
478
+ """
479
+ Initialize the transformer and set up a placeholder for a lazily-loaded resolver.
480
+
481
+ Sets self._resolver to None to indicate the resolver has not been loaded yet.
482
+ """
483
+ self._resolver = None
397
484
 
398
485
  def accepts(self, info, file_pair):
486
+ """
487
+ Determine whether the transformer should run for the given property info by checking for a fourth parameter.
488
+
489
+ Parameters:
490
+ info (dict): Parsed property metadata; expects a "params" list when present.
491
+ file_pair (tuple): Source/implementation file pair (unused by this check).
492
+
493
+ Returns:
494
+ `true` if `info["params"]` exists and contains at least four items, `false` otherwise.
495
+ """
399
496
  return info.get("params") and len(info["params"]) > 3
400
497
 
498
+ def _get_resolver(self):
499
+ """
500
+ Lazily load and cache the constexpr identifier resolver.
501
+
502
+ Returns:
503
+ callable or None: The resolver function if available, or `None` if it could not be loaded.
504
+ """
505
+ if self._resolver is None:
506
+ resolver = get_resolve_constexpr_identifier()
507
+ self._resolver = resolver if resolver else False
508
+ return self._resolver if self._resolver is not False else None
509
+
510
+ def _resolve_identifier(self, identifier):
511
+ """
512
+ Resolve a constexpr identifier to its corresponding string value.
513
+
514
+ Parameters:
515
+ identifier (str): Identifier to resolve (for example, "scram" or "gssapi").
516
+
517
+ Returns:
518
+ str or None: The resolved string value if successful, `None` when the identifier is invalid or cannot be resolved.
519
+ """
520
+ if not identifier or not isinstance(identifier, str):
521
+ logger.warning(f"Invalid identifier for resolution: {identifier}")
522
+ return None
523
+
524
+ resolver = self._get_resolver()
525
+ if resolver:
526
+ try:
527
+ return resolver(identifier)
528
+ except (AttributeError, TypeError, ValueError) as e:
529
+ logger.debug(f"Failed to resolve identifier '{identifier}': {e}")
530
+ except Exception as e:
531
+ logger.exception(f"Unexpected error resolving identifier '{identifier}': {e}")
532
+
533
+ return None
534
+
535
+ def _process_sstring_constructor(self, item):
536
+ """
537
+ Convert an ss::sstring{identifier} constructor string to its resolved value when possible.
538
+
539
+ If the input matches the ss::sstring{...} pattern, attempts to resolve the enclosed identifier and returns the resolved string. If resolution fails, returns the raw identifier. If the input does not match the pattern or is falsy, returns it unchanged.
540
+
541
+ Parameters:
542
+ item (str): The constructor expression or string to process.
543
+
544
+ Returns:
545
+ str: The resolved string when resolution succeeds, the extracted identifier if resolution fails, or the original input if it does not match.
546
+ """
547
+ if not item:
548
+ return item
549
+
550
+ match = re.match(self.SSTRING_CONSTRUCTOR_PATTERN, item)
551
+ if not match:
552
+ return item
553
+
554
+ identifier = match.group(1)
555
+ resolved = self._resolve_identifier(identifier)
556
+
557
+ if resolved:
558
+ logger.debug(f"Resolved ss::sstring{{{identifier}}} -> '{resolved}'")
559
+ return resolved
560
+
561
+ # Log warning but continue with original identifier
562
+ logger.warning(f"Could not resolve identifier '{identifier}' in ss::sstring constructor")
563
+ return identifier
564
+
565
+ def _parse_vector_contents(self, contents):
566
+ """
567
+ Parse a comma-separated std::vector initializer string into a list of cleaned, processed items.
568
+
569
+ Parameters:
570
+ contents (str): The inner contents of a vector initializer (e.g. '\"a\", ss::sstring{ID}, \"b\"'); may be empty or None.
571
+
572
+ Returns:
573
+ list: Ordered list of processed, unquoted items with empty entries omitted.
574
+ """
575
+ if not contents:
576
+ return []
577
+
578
+ # Split by comma and process each item
579
+ raw_items = [contents] if ',' not in contents else contents.split(',')
580
+
581
+ processed_items = []
582
+ for item in raw_items:
583
+ item = item.strip(' "\'')
584
+ if item: # Skip empty items
585
+ processed_item = self._process_sstring_constructor(item)
586
+ processed_items.append(processed_item)
587
+
588
+ return processed_items
589
+
401
590
  def parse(self, property, info, file_pair):
591
+ """
592
+ Convert a C++ default expression into a JSON-friendly value and store it on the property under the "default" key.
593
+
594
+ Parameters:
595
+ property (dict): Property dictionary to modify; updated in place with a "default" entry.
596
+ info (dict): Parsed property information; the default expression is expected at info["params"][3]["value"].
597
+ file_pair: File pair context (ignored by this function).
598
+
599
+ Returns:
600
+ dict: The modified property dictionary with a normalized "default" value.
601
+ """
402
602
  default = info["params"][3]["value"]
603
+
604
+ # Handle null/empty defaults
605
+ if not default:
606
+ return property
403
607
 
404
- # Transform std::nullopt into None.
608
+ # Transform std::nullopt into None
405
609
  if "std::nullopt" in default:
406
610
  property["default"] = None
407
611
  return property
408
612
 
409
- # Transform std::numeric_limits expressions.
613
+ # Transform std::numeric_limits expressions
410
614
  if "std::numeric_limits" in default:
411
615
  property["default"] = "Maximum value"
412
616
  return property
413
617
 
414
- # Transform std::chrono durations.
618
+ # Transform std::chrono durations
415
619
  if "std::chrono" in default:
416
- m = re.search(r"std::chrono::(\w+)\(([^)]+)\)", default)
417
- if m:
418
- unit = m.group(1)
419
- value = m.group(2).strip()
620
+ match = re.search(self.CHRONO_PATTERN, default)
621
+ if match:
622
+ unit = match.group(1)
623
+ value = match.group(2).strip()
420
624
  property["default"] = f"{value} {unit}"
421
625
  return property
422
626
 
423
- # Transform std::vector defaults.
627
+ # Transform std::vector defaults
424
628
  if self.ARRAY_PATTERN_STD_VECTOR in default:
425
- m = re.search(r'\{([^}]+)\}', default)
426
- if m:
427
- contents = m.group(1).strip()
428
- items = [item.strip(' "\'') for item in contents.split(',')]
629
+ vector_match = re.search(self.VECTOR_INITIALIZER_PATTERN, default)
630
+ if vector_match:
631
+ contents = vector_match.group(1).strip()
632
+ items = self._parse_vector_contents(contents)
429
633
  property["default"] = items
430
634
  return property
431
635
 
432
- # Otherwise, leave the default as-is.
636
+ # For all other cases, leave the default as-is
433
637
  property["default"] = default
434
638
  return property
435
639
 
@@ -566,4 +770,4 @@ class MetaParamTransformer:
566
770
  key, value = item.split('=')
567
771
  meta_dict[key.strip().replace('.', '')] = value.strip()
568
772
  meta_dict['type'] = 'initializer_list' # Enforce required type
569
- param['value'] = meta_dict
773
+ param['value'] = meta_dict